import math
import os
import glob
import json
import PyQt5.sip as sip
from PyQt5.QtCore import pyqtSlot
import logging
import numpy as np
import cv2
import traceback
from PyQt5.QtWidgets import QFormLayout
from PyQt5.QtWidgets import QProgressDialog
from PyQt5.QtWidgets import QProgressBar
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QTextEdit
from collections import defaultdict
from PyQt5.QtWidgets import (
    QGraphicsView, QGraphicsScene, QGraphicsRectItem, QWidget, 
    QHBoxLayout, QVBoxLayout, QLabel, QPushButton, QSlider, QLineEdit, 
    QListWidget, QMessageBox, QTreeWidget, QTreeWidgetItem, QDialog, 
    QPlainTextEdit, QCheckBox, QGroupBox, QRadioButton, QButtonGroup,
    QSplitter, QSizePolicy, QComboBox, QGridLayout, QSpinBox, QTabWidget, QFileDialog, QDoubleSpinBox, QShortcut, QToolButton, QFrame
)
from PyQt5.QtCore import Qt, QRectF, pyqtSignal, QTimer, QPointF, QSizeF
from PyQt5.QtGui import QPen, QColor, QImage, QPixmap, QIcon, QKeySequence
from core.alignment.mmaligment import center_crop_resize
from core.utils.image_utils import cv2_to_qpixmap

# Configure logging
logger = logging.getLogger(__name__)
handler = logging.StreamHandler()
formatter = logging.Formatter('[%(levelname)s] %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)

# Constants
DEFAULT_COLORS = {
    1: "#FF0000",  # Red
    2: "#00FF00",  # Green
    3: "#0000FF",  # Blue
    4: "#FFFF00",  # Yellow
    5: "#00FFFF",  # Cyan
    6: "#FF00FF",  # Magenta
    7: "#FFA500",  # Orange
    8: "#800080",  # Purple
    9: "#A52A2A",  # Brown
    10: "#808080"  # Gray
}

def load_frame_metadata(metadata_file: str) -> dict:
    """Load and parse extracted frame metadata from JSON."""
    try:
        with open(metadata_file, 'r', encoding='utf-8') as f:
            metadata = json.load(f)
        return metadata
    except Exception as e:
        logger.error(f"Unable to load metadata: {e}")
        return {}

def display_frame_metadata(metadata: dict):
    """
    Display parsed metadata in the annotation tool.
    In this demo, we simply print each frame's info.
    """
    frames = metadata.get("frames", [])
    if not frames:
        logger.info("No frame metadata available.")
        return

    for frame in frames:
        md = frame.get("metadata", {})
        logger.info(f"Frame {frame.get('original_frame')}:")
        logger.info(f"  Video Timestamp: {frame.get('video_timestamp', '')}")
        logger.info(f"  SRT Timestamp: {frame.get('srt_timestamp', '')}")
        logger.info(f"  Diff Time: {frame.get('diff_time', '')}")
        logger.info(f"  Camera - focal_len: {md.get('focal_len', '')}, dzoom_ratio: {md.get('dzoom_ratio', '')}")
        logger.info(f"  GPS - latitude: {md.get('latitude', '')}, longitude: {md.get('longitude', '')}, rel_alt: {md.get('rel_alt', '')}, abs_alt: {md.get('abs_alt', '')}")
        logger.info(f"  Attitude - yaw: {md.get('gb_yaw', '')}, pitch: {md.get('gb_pitch', '')}, roll: {md.get('gb_roll', '')}")
        logger.info("-" * 40)

class AnnotatableView(QGraphicsView):
    """
    A custom QGraphicsView that supports drawing annotations (bounding boxes)
    and managing them by color ID.
    """
    annotationChanged = pyqtSignal(QRectF, int, QColor)
    
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setScene(QGraphicsScene(self))
        self._startPos = None
        self._currentRectItem = None
        self._annotations = []
        self.setDragMode(QGraphicsView.NoDrag)
        self.annotation_enabled = False
        self.current_color_id = None
        self.current_color = None
        # Modified: Use nested dictionaries to store annotations by color_id and modality
        self.color_annotations = {}  # color_id -> list of annotations
        self.autoFit = True
        self.setAlignment(Qt.AlignCenter)
        self._annotation_widget = None  # 新添加的属性
        self.selection_mode = False  # 新添加的属性
    
    # 添加设置和获取annotation_widget的方法
    def set_annotation_widget(self, widget):
        """设置对AnnotationWidget的引用"""
        self._annotation_widget = widget

    # 修改AnnotatableView类的mouseDoubleClickEvent方法
    def mouseDoubleClickEvent(self, event):
        """处理鼠标双击事件，选中被点击的标注框"""
        # 获取点击位置的所有项目
        items = self.items(event.pos())
        
        # 筛选出矩形项目（标注框）
        rect_items = [item for item in items if isinstance(item, QGraphicsRectItem)]
        
        if (rect_items):
            # 获取第一个矩形项
            rect_item = rect_items[0]
            
            # 检查是否有annotation_data属性
            if hasattr(rect_item, 'annotation_data'):
                # 获取标注数据
                color_id = rect_item.annotation_data.get('color_id')
                modality = rect_item.annotation_data.get('modality')
                
                if color_id is not None and modality is not None:
                    # 调用父窗口的方法选中此标注
                    parent = self.get_annotation_widget()
                    if parent:
                        parent.select_annotation_by_id(color_id, modality)
                        return
        
        # 如果没有在标注框上双击，则调用父类方法
        super().mouseDoubleClickEvent(event)


    def get_annotation_widget(self):
        """获取AnnotationWidget引用，无论嵌套层次如何"""
        if hasattr(self, '_annotation_widget') and self._annotation_widget:
            return self._annotation_widget
            
        # 如果没有直接引用，尝试在父级链中查找
        parent = self.parent()
        while parent:
            # 检查是否有log_message方法，这是AnnotationWidget的特征
            if hasattr(parent, 'log_message'):
                return parent
            # 继续向上查找
            parent = parent.parent()
            
        # 如果找不到，返回None
        return None
    
    def set_annotation_enabled(self, enabled):
        """Enable or disable annotation mode"""
        self.annotation_enabled = enabled
    
    def set_current_color(self, color_id, color):
        """Set the current annotation color"""
        self.current_color_id = color_id
        self.current_color = color
        
    def clear_selection_states(self):
        """清除所有选择状态"""
        # 取消颜色选择
        self.annotation_button_group.setExclusive(False)
        for btn in self.annotation_button_group.buttons():
            btn.setChecked(False)
        self.annotation_button_group.setExclusive(True)
        
        # 清除树选择
        self.bbox_tree.clearSelection()
        
        # 清除高亮
        if self.highlighted_item:
            try:
                scene = self.highlighted_item.scene()
                if scene:
                    scene.removeItem(self.highlighted_item)
            except RuntimeError:
                pass
            self.highlighted_item = None
        
        # 禁用标注功能
        self.rgb_view.set_annotation_enabled(False)
        self.ti_view.set_annotation_enabled(False)

    def mousePressEvent(self, event):
        # 获取父类引用
        parent = self.get_annotation_widget()
    
        # 处理中键点击 - 取消选中
        if event.button() == Qt.MidButton:
            if parent:
                parent.deselect_annotation()
                event.accept()
                return
    
        # 处理右键点击 - 选中标注
        elif event.button() == Qt.RightButton:
            # 获取点击位置的所有项目
            scene_pos = self.mapToScene(event.pos())
            items = self.scene().items(scene_pos)
        
            # 筛选出矩形项目（标注框）
            rect_items = [item for item in items if isinstance(item, QGraphicsRectItem)]
        
            if rect_items and parent:
                # 找到第一个带annotation_data的标注框
                for rect_item in rect_items:
                    if hasattr(rect_item, 'annotation_data'):
                        # 获取标注数据
                        color_id = rect_item.annotation_data.get('color_id')
                        modality = rect_item.annotation_data.get('modality')
                    
                        if color_id is not None and modality is not None:
                            # 调用选择方法
                            parent.select_annotation_by_id(color_id, modality)
                            event.accept()
                            return
        
            # 如果右键点击空白区域也取消选择
            if parent and parent.highlighted_item:
                parent.deselect_annotation()
    
        # 如果当前有高亮标注，则阻止新标注
        if parent and parent.highlighted_item and event.button() == Qt.LeftButton:
            parent.log_message("当前存在选中标注，请先取消选择")
            return
    
        # 处理选择模式
        if hasattr(self, '_selection_mode') and self._selection_mode:
            # 在选择模式下，点击时进行现有标注框的选择
            scene_pos = self.mapToScene(event.pos())
            items = self.scene().items(scene_pos)
            rect_items = [item for item in items if isinstance(item, QGraphicsRectItem)]
        
            # 获取AnnotationWidget引用
            parent = self._annotation_widget
        
            if rect_items and parent:
                # 找到第一个带annotation_data的标注框
                for rect_item in rect_items:
                    if hasattr(rect_item, 'annotation_data'):
                        # 获取标注数据
                        color_id = rect_item.annotation_data.get('color_id')
                        modality = rect_item.annotation_data.get('modality')
                    
                        if color_id is not None and modality is not None:
                            # 调用选择方法
                            QTimer.singleShot(10, lambda: parent.select_annotation_by_id(color_id, modality))
                            event.accept()
                            return
                        
            # 没有找到可点击的项目时，调用父类方法
            super().mousePressEvent(event)
            return
    
        # 原有的左键标注逻辑保持不变
        if event.button() == Qt.LeftButton and self.annotation_enabled and self.current_color_id is not None:
            # 这里是原有的标注代码，保持不变
            parent = self._annotation_widget
        
            # 如果_annotation_widget没有设置，尝试逐层向上查找
            if parent is None:
                temp = self.parent()
                while temp and not hasattr(temp, 'log_message'):
                    temp = temp.parent()
                parent = temp
            
            if parent and hasattr(parent, 'log_message'):
                parent.log_message(f"开始绘制 ID{self.current_color_id} 标注")
        
            # 原有的绘制标注代码...
            # Check if there's an existing annotation with the same ID
            if hasattr(parent, 'annotations'):
                existing_annotation = None
                for ann in parent.annotations:
                    if (ann.get("frame") == parent.current_frame_index and 
                        ann.get("color_id") == self.current_color_id and
                        ((self == parent.rgb_view and ann.get("modality") == "RGB") or
                        (self == parent.ti_view and ann.get("modality") == "TI"))):
                    
                        existing_annotation = ann
                        break
                    
                if existing_annotation:
                    parent.log_message(f"注意: 正在更新ID {self.current_color_id} 的现有标注")
        
            # 清除高亮框并继续绘制新标注...
            if hasattr(parent, 'highlighted_item') and parent.highlighted_item:
                try:
                    scene = parent.highlighted_item.scene()
                    if scene and not sip.isdeleted(parent.highlighted_item):
                        scene.removeItem(parent.highlighted_item)
                except RuntimeError:
                    pass
                parent.highlighted_item = None
            
            # Clear tree selection
            if hasattr(parent, 'bbox_tree'):
                parent.bbox_tree.blockSignals(True)
                parent.bbox_tree.clearSelection()  
                parent.bbox_tree.blockSignals(False)
        
            # Clear preview selection flag
            if hasattr(parent, '_preview_selected_annotation'):
                delattr(parent, '_preview_selected_annotation')
            
            # Clear existing annotations for the current color ID
            self.clearColorAnnotation(self.current_color_id)
        
            # Start drawing new annotation
            self._startPos = self.mapToScene(event.pos())
            self._currentRectItem = QGraphicsRectItem(QRectF(self._startPos, self._startPos))
            pen = QPen(QColor(self.current_color or "red"))
            pen.setWidth(2)
            self._currentRectItem.setPen(pen)
            self.scene().addItem(self._currentRectItem)
        else:
            # 原有的其他代码...
            if self.annotation_enabled == False:
                # 使用get_annotation_widget()来获取AnnotationWidget实例
                parent = self.get_annotation_widget()
                if parent and hasattr(parent, 'log_message'):
                    parent.log_message(f"标注功能未启用，请选择一个颜色")
                else:
                    # 如果找不到恰当的父对象，则使用print记录消息
                    print(f"标注功能未启用，请选择一个颜色")
        
        # 调用父类方法处理默认行为
        super().mousePressEvent(event)
        
    def mouseMoveEvent(self, event):
        if self._currentRectItem and not sip.isdeleted(self._currentRectItem):
            currentPos = self.mapToScene(event.pos())
            rect = QRectF(self._startPos, currentPos).normalized()
            self._currentRectItem.setRect(rect)
        super().mouseMoveEvent(event)

    def clearAnnotations(self, keep_highlight=False):
        """清除标注，可选保留高亮框"""
        # 清除临时框
        if self._currentRectItem:
            self.scene().removeItem(self._currentRectItem)
            self._currentRectItem = None
        
        # 清除颜色标注（排除高亮框）
        for color_id in list(self.color_annotations.keys()):
            items_to_remove = []
            for item in self.color_annotations[color_id]:
                if not keep_highlight or item != self._annotation_widget.highlighted_item:
                    try:
                        if not sip.isdeleted(item) and item.scene():
                            self.scene().removeItem(item)
                            items_to_remove.append(item)
                    except RuntimeError:
                        pass
            # 更新列表
            self.color_annotations[color_id] = [
                item for item in self.color_annotations[color_id]
                if item not in items_to_remove
            ]
        
        # 清除通用列表
        self._annotations = [
            item for item in self._annotations
            if not keep_highlight or item != self._annotation_widget.highlighted_item
        ]

    # 修改AnnotatableView的mouseReleaseEvent方法，避免在同步标注时出现重复框
    def mouseReleaseEvent(self, event):
        """处理鼠标释放事件，用于完成标注绘制"""
        if event.button() == Qt.LeftButton and self._currentRectItem and self.annotation_enabled:
            # 获取矩形
            rect = self._currentRectItem.rect()
            parent = self.get_annotation_widget()
            
            # 检查是否为点击操作（矩形太小）
            is_small_rect = rect.width() < 5 or rect.height() < 5
            
            # 如果矩形太小，视为点击，清除并返回
            if is_small_rect:
                self.scene().removeItem(self._currentRectItem)
                self._currentRectItem = None
                super().mouseReleaseEvent(event)
                return
            
            # 添加到颜色特定的标注列表
            if self.current_color_id is not None:
                if self.current_color_id not in self.color_annotations:
                    self.color_annotations[self.current_color_id] = []
                self.color_annotations[self.current_color_id].append(self._currentRectItem)
            
            # 添加到通用标注列表
            self._annotations.append(self._currentRectItem)
            
            # 发送带标注细节的信号
            if self.current_color_id is not None and self.current_color is not None:
                self.annotationChanged.emit(rect, self.current_color_id, QColor(self.current_color))
            else:
                self.annotationChanged.emit(rect, -1, QColor("#FFFFFF"))
            
            # 完成后重置临时框引用
            self._currentRectItem = None
        elif self._currentRectItem:
            # 如果释放鼠标但不符合条件，清除临时框
            self.scene().removeItem(self._currentRectItem)
            self._currentRectItem = None
            
        super().mouseReleaseEvent(event)
    
    def clearColorAnnotation(self, color_id, modality=None):
        """Clear all annotations for the specified color ID and optionally by modality"""
        if color_id in self.color_annotations:
            items_to_remove = []
            # 先收集所有要删除的item
            for item in self.color_annotations[color_id]:
                try:
                    # Only remove items matching the modality if specified
                    item_data = getattr(item, 'annotation_data', {})
                    if modality is None or item_data.get('modality') == modality:
                        if not sip.isdeleted(item) and item.scene() is not None:
                            self.scene().removeItem(item)
                            # 记录要从_annotations中删除的项目
                            items_to_remove.append(item)
                except RuntimeError:
                    pass
            
            # 从通用标注列表中也删除这些项目
            for item in items_to_remove:
                if item in self._annotations:
                    self._annotations.remove(item)
            
            # If modality is specified, only remove annotations for that modality
            if modality is not None:
                # Keep annotations for other modalities
                self.color_annotations[color_id] = [
                    item for item in self.color_annotations[color_id] 
                    if getattr(item, 'annotation_data', {}).get('modality') != modality
                ]
            else:
                # Clear all annotations for this color_id
                self.color_annotations[color_id] = []
    
    def clearAnnotations(self):
        """清除所有标注框"""
        # 清除临时绘制的框
        if self._currentRectItem and not sip.isdeleted(self._currentRectItem):
            self.scene().removeItem(self._currentRectItem)
            self._currentRectItem = None
        
        # 清除颜色特定的标注
        for color_id in list(self.color_annotations.keys()):
            self.clearColorAnnotation(color_id)
        self.color_annotations = {}
        
        # 清除通用标注列表
        while self._annotations:
            item = self._annotations.pop()
            try:
                if not sip.isdeleted(item) and item.scene() is not None:
                    self.scene().removeItem(item)
            except RuntimeError:
                pass

    def showEvent(self, event):
        """Handle show event by fitting content to view if needed"""
        if self.scene() and self.scene().items():
            self.fitInView(self.scene().items()[0], Qt.KeepAspectRatio)
        super().showEvent(event)
    
    def resizeEvent(self, event):
        """Handle resize event by fitting content to view if auto-fit is enabled"""
        if self.autoFit:
            if self.scene() and self.scene().items():
                self.fitInView(self.scene().itemsBoundingRect(), Qt.KeepAspectRatio)
        super().resizeEvent(event)
    
    def setPixmap(self, pixmap):
        """Set the pixmap for this view and fit it to the view"""
        self.scene().clear()
        self.scene().addPixmap(pixmap)
        if self.autoFit:
            self.fitInView(self.scene().itemsBoundingRect(), Qt.KeepAspectRatio)

    def drawAnnotation(self, rect, color, color_id=None, modality=None):
        """Draw an annotation rectangle with the specified color"""
        pen = QPen(QColor(color))
        pen.setWidth(2)
        rect_item = QGraphicsRectItem(rect)
        rect_item.setPen(pen)
        self.scene().addItem(rect_item)
        self._annotations.append(rect_item)
        
        # Store modality information with the annotation
        if modality is not None:
            rect_item.annotation_data = {'color_id': color_id, 'modality': modality}
        
        # 将标注框添加到对应颜色的列表中
        if color_id is not None:
            if color_id not in self.color_annotations:
                self.color_annotations[color_id] = []
            self.color_annotations[color_id].append(rect_item)
        
        return rect_item

    # Add this method to AnnotatableView class if missing
    def set_selection_mode(self, enabled):
        """Enable or disable selection mode (select but don't draw)"""
        self._selection_mode = enabled

import math

def generate_viewpoint_hash(metadata, gps_sensitivity=2):
    """
    Generate a unique viewpoint hash based on drone position and orientation.
    
    Parameters:
    - metadata: Dictionary containing drone telemetry data
    - gps_sensitivity: Value 1-5 controlling how sensitive the function is to position changes
                       1: ~200m grids, 2: ~100m, 3: ~50m, 4: ~30m, 5: ~10m
    """
    if not metadata:
        return None

    # Extract position data
    lat = metadata.get('latitude')
    lon = metadata.get('longitude')
    alt = metadata.get('rel_alt')  # Relative altitude
    abs_alt = metadata.get('abs_alt')  # Absolute altitude (sea level)
    yaw = metadata.get('gb_yaw')   # Gimbal yaw
    pitch = metadata.get('gb_pitch')  # Gimbal pitch

    # Skip if essential GPS data is missing
    if not all([lat, lon]):
        return None

    try:
        # Convert string values to float if needed
        lat = float(lat) if isinstance(lat, str) else lat
        lon = float(lon) if isinstance(lon, str) else lon
        alt = float(alt) if isinstance(alt, str) and alt else 0
        abs_alt = float(abs_alt) if isinstance(abs_alt, str) and abs_alt else 0
        yaw = float(yaw) if isinstance(yaw, str) and yaw else 0
        pitch = float(pitch) if isinstance(pitch, str) and pitch else 0

        # 重新设计GPS灵敏度参数（单位：米）
        grid_sizes = {
            1: 200,   # 200m网格
            2: 100,   # 100m 
            3: 50,    # 50m
            4: 30,    # 30m
            5: 10     # 10m
        }
        target_grid_size = grid_sizes.get(gps_sensitivity, 10)

        # 动态计算纬度乘数（1度≈111km）
        lat_mult = 1 / (target_grid_size / 111_000)  # 每米对应的度数倒数

        # 经度乘数补偿纬度影响
        lon_mult = lat_mult / abs(math.cos(math.radians(lat)))

        # 区域划分使用主网格（不再额外放大）
        zone_lat = int(lat * lat_mult)
        zone_lon = int(lon * lon_mult)
        
        # 新增：将经纬度合并为一个位置标识
        # 使用Cantor配对函数将两个整数映射到一个唯一整数
        # (x,y) -> (1/2)(x+y)(x+y+1)+y
        geo_zone_id = int((0.5 * (zone_lat + zone_lon) * (zone_lat + zone_lon + 1)) + zone_lon)

        # 修改5：姿态划分更详细，尤其是重要的偏航角
        yaw_norm = (yaw + 360) % 360
        # 180度一个区间，非常粗粒度
        direction = int(yaw_norm / 180) 

        # 修改：俯仰角使用更宽松的粗粒度划分，只分为2类
        if pitch < -30:
            view_angle = 0  # 向下看
        else:
            view_angle = 1  # 平视或向上看

        # 优先使用区域作为主要分组因素，然后是方向和视角
        # 使用组合后的geo_zone_id代替原来的(zone_lat, zone_lon)
        viewpoint_hash = (geo_zone_id, direction, view_angle)
        return viewpoint_hash

    except Exception as e:
        print(f"Error generating viewpoint hash: {e}")
        return None

class CameraIdMapper:
    def __init__(self, gps_sensitivity=2):
        self.viewpoint_hashes = set()  # 只用来跟踪已经看到的哈希值
        self.gps_sensitivity = gps_sensitivity
        self.last_hash_details = {}  # 记录最近一次哈希的详细计算值
        # 存储原始的zone_lat和zone_lon，用于日志
        self._last_raw_zones = {}

    def _format_hash(self, hash_tuple):
        """将哈希元组格式化为可读字符串"""
        if not hash_tuple:
            return "未知"
        # 使用简单格式：位置ID_方向_视角
        return f"{hash_tuple[0]}_{hash_tuple[1]}_{hash_tuple[2]}"

    def _log_hash_change(self, new_hash, metadata):
        """专用日志方法，记录哈希变化详情"""
        print("\n" + "="*40)
        is_first_hash = len(self.viewpoint_hashes) == 0
        
        formatted_hash = self._format_hash(new_hash)
        
        if is_first_hash:
            print(f"场景ID创建: {formatted_hash} (第一个视角)")
        else:
            last_hash = self.last_hash_details.get("hash")
            last_formatted = self._format_hash(last_hash) if last_hash else "未知"
            print(f"场景ID变更: {last_formatted} → {formatted_hash}")
        
        print("Change reason analysis:")
        
        # 计算原始的zone_lat和zone_lon值，用于日志显示
        lat = float(metadata.get('latitude', 0))
        lon = float(metadata.get('longitude', 0))
        target_grid_size = {1: 200, 2: 100, 3: 50, 4: 30, 5: 10}.get(self.gps_sensitivity, 10)
        lat_mult = 1 / (target_grid_size / 111_000)
        lon_mult = lat_mult / abs(math.cos(math.radians(lat)))
        zone_lat = int(lat * lat_mult)
        zone_lon = int(lon * lon_mult)
        
        # 存储原始区域值用于比较
        self._last_raw_zones = {"zone_lat": zone_lat, "zone_lon": zone_lon}
        
        # 提取新旧值对比
        old_values = self.last_hash_details.get("components", {})
        new_values = {
            "geo_zone_id": new_hash[0],
            "zone_lat": zone_lat,  # 添加原始区域值，方便理解
            "zone_lon": zone_lon,  # 添加原始区域值，方便理解
            "direction": new_hash[1],
            "view_angle": new_hash[2]
        }
        
        # 对比各维度变化
        changed = []
        for key in ["geo_zone_id", "direction", "view_angle"]:
            old = old_values.get(key)
            new = new_values[key]
            if old != new:
                changed.append(f"{key}: {old if old is not None else 'Initial'} → {new}")
        
        # 检测原始经纬度区域变化
        old_lat = self.last_hash_details.get("components", {}).get("zone_lat")
        old_lon = self.last_hash_details.get("components", {}).get("zone_lon")
        if old_lat != zone_lat or old_lon != zone_lon:
            changed.append(f"位置: ({old_lat},{old_lon}) → ({zone_lat},{zone_lon})")
        
        # 打印关键参数原始值
        lat = metadata.get('latitude', 'N/A')
        lon = metadata.get('longitude', 'N/A')
        rel_alt = metadata.get('rel_alt', 'N/A')
        yaw = metadata.get('gb_yaw', 'N/A')
        pitch = metadata.get('gb_pitch', 'N/A')
        
        print(f"Raw GPS Data: lat={lat}, lon={lon}")
        print(f"Altitude: {rel_alt}m, Orientation: yaw={yaw}°, pitch={pitch}°")
        print(f"Processed Components: {new_values}")
        print(f"Changed Dimensions: {changed if changed else '(No changes)'}")
        
        # 记录当前详细值
        self.last_hash_details = {
            "hash": new_hash,
            "components": new_values,
            "raw_data": {
                "lat": metadata.get('latitude'),
                "lon": metadata.get('longitude'),
                "alt": metadata.get('rel_alt'),
                "abs_alt": metadata.get('abs_alt'),
                "yaw": metadata.get('gb_yaw'),
                "pitch": metadata.get('gb_pitch')
            }
        }

    def get_camera_id(self, metadata):
        """根据元数据获取场景ID，直接使用哈希值"""
        viewpoint_hash = generate_viewpoint_hash(metadata, self.gps_sensitivity)
        if viewpoint_hash is None:
            print("Warning: Invalid metadata received")
            return None
        
        # 如果是新哈希值，记录日志
        if viewpoint_hash not in self.viewpoint_hashes:
            self._log_hash_change(viewpoint_hash, metadata)
            self.viewpoint_hashes.add(viewpoint_hash)
            
        # 返回格式化后的哈希字符串作为ID
        return self._format_hash(viewpoint_hash)

class AnnotationWidget(QWidget):
    """
    Main widget for the annotation tool containing two annotatable views (RGB and TI)
    with controls for navigation, annotation, and metadata display.
    """
    offsetChanged = pyqtSignal(int, int)  # signal for offset synchronization

    def __init__(self, parent=None):
        super().__init__(parent)
        
        # Add camera ID mapper initialization
        self.camera_id_mapper = CameraIdMapper()
        
        # 添加快捷键开关和状态标记
        self.shortcuts_enabled = False
        self.shortcuts = []
        self.selected_annotation = None  # 当前选中的标注框
        
        # 保存调整布局的引用
        self.adjustment_layout = None  # 存储调整布局的引用
        self.edit_group = None  # 存储编辑组的引用
        
        # Initialize state variables
        self.current_project = ""
        self.rgb_frames = []
        self.ti_frames = []
        self.two_column_mode = False
        self.current_single_mode = "RGB"
        self.current_frame_index = 0
        self.loaded_metadata = None
        self.timer = None
        self.loaded_video = None
        self.playing = False
        self.autoFit = True
        self.child_autoFit = True
        self.simultaneous_annotation = False
        self.drawn_annotations = {}  # Map reid -> current drawn QGraphicsRectItem
        self.color_map = DEFAULT_COLORS.copy()
        
        # Load annotation manager
        try:
            from core.annotator.annotation_manager import AnnotationManager
            self.annotation_manager = AnnotationManager()
        except ImportError:
            logger.error("Failed to import AnnotationManager. Annotation saving may not work.")
            self.annotation_manager = None
            
        self.annotations = []  # All annotations in this view
        self.selected_color_id = None
        self.highlighted_item = None  # Currently highlighted item
        
        # Add annotation caching and tracking variables
        self.annotation_cache = {}  # Video ID -> {frame -> annotations}
        self.cached_video_id = None
        self.annotation_modified = False
        self.is_loading_annotations = False
        
        # Create image views panel
        self.images_panel = QSplitter(Qt.Vertical)
        self.rgb_view = AnnotatableView()
        self.ti_view = AnnotatableView()
        
        # 设置对AnnotationWidget的引用
        self.rgb_view.set_annotation_widget(self)
        self.ti_view.set_annotation_widget(self)
        
        self.rgb_view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.ti_view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        
        # Initialize UI
        self.init_ui()
        self._setup_shortcuts()  # 设置快捷键，但默认不激活
        
    @pyqtSlot(str)
    def open_from_json(self, json_data):
        """Handle the goto annotation signal with JSON data"""
        print(f"Annotator received JSON data: {json_data}")
        
        try:
            import json
            data = json.loads(json_data)
            
            video_path = data.get("video_path")
            frame_folder = data.get("frame_folder")
            
            print(f"Annotator processing: video_path={video_path}, frame_folder={frame_folder}")
            
            if frame_folder and os.path.exists(frame_folder):
                print(f"Opening frame folder: {frame_folder}")
                # Your existing code to load frames from this folder
                self.load_frames_from_folder(frame_folder)
                self.load_video(video_path)
                print(f"Successfully opened {frame_folder}")
            elif video_path and os.path.exists(video_path):
                print(f"Frame folder not found, opening video only: {video_path}")
                # Your existing code to open just the video
                self.load_video(video_path)
            else:
                print(f"Error: Neither frame folder nor video path is valid")
        
        except Exception as e:
            import traceback
            print(f"Error processing annotation signal: {str(e)}")
            print(traceback.format_exc())

    def select_annotation_by_id(self, color_id, modality):
        """根据ID和模态选择标注"""
        try:
            # 先清除所有已有选择
            self.deselect_annotation()
            
            # 找到对应的标注
            found_ann = None
            for ann in self.annotations:
                if (ann.get("frame") == self.current_frame_index and 
                    ann.get("color_id") == color_id and
                    ann.get("modality") == modality):
                    found_ann = ann
                    break
                    
            if found_ann:
                # 在树中找到并选择对应的项
                for i in range(self.bbox_tree.topLevelItemCount()):
                    item = self.bbox_tree.topLevelItem(i)
                    item_ann = item.data(0, Qt.UserRole)
                    if (item_ann and 
                        item_ann.get("color_id") == color_id and 
                        item_ann.get("modality") == modality):
                        # 通过设置选中状态来触发on_annotation_selected_in_tree
                        self.bbox_tree.setCurrentItem(item)
                        return True
            
            return False
        except Exception as e:
            self.log_message(f"选择标注时出错: {str(e)}")
            return False


    def load_video(self, video_path):
        """Load a video by its path (called from outside the widget)"""
        # Handle case where video_path is a JSON string or dict
        video_info = None
        if isinstance(video_path, str) and (video_path.startswith('{') and video_path.endswith('}')):
            try:
                video_info = json.loads(video_path)
                actual_video_path = video_info.get("video_path")
                base_frame_folder = video_info.get("base_frame_folder")
                frame_folders = video_info.get("frame_folders", {})
                selected_modality = video_info.get("selected_modality")
                
                # Log received information
                self.log_message(f"接收到视频信息: 视频路径={actual_video_path}, 帧文件夹={base_frame_folder}")
                
                # Check if frame folders exist even if video doesn't
                frame_folder_exists = base_frame_folder and os.path.exists(base_frame_folder)
                modality_folder_exists = False
                
                for mod, folder in frame_folders.items():
                    if folder and os.path.exists(folder):
                        modality_folder_exists = True
                        break
                        
                if not actual_video_path or not os.path.exists(actual_video_path):
                    self.log_message(f"警告: 视频文件不存在或未提供: {actual_video_path}")
                    # Proceed anyway if we have valid frame folders
                    if frame_folder_exists or modality_folder_exists:
                        self.log_message(f"将使用帧文件夹: {base_frame_folder}")
                        # Set video_path to None to skip video file validation
                        video_path = actual_video_path
                    else:
                        self.log_message(f"错误: 视频文件和帧文件夹都不可用")
                        return False
                else:
                    video_path = actual_video_path
            except Exception as e:
                self.log_message(f"解析视频信息失败: {str(e)}")
        """Load a video by its path (called from outside the widget)"""
        if not video_path or not os.path.exists(video_path):
            self.log_message(f"无效的视频路径: {video_path}")
            return False
            
        # Check if video list is populated
        if self.video_list.topLevelItemCount() == 0:
            # Video list is empty, we need to set the project first
            # Try to determine the project path from the video path
            if self.current_project:
                # Already have a project set, continue with video loading
                pass
            else:
                # Try to find project path from video path
                videos_dir = None
                path_parts = video_path.split(os.sep)
                for i in range(len(path_parts)-1, 0, -1):
                    if path_parts[i] == 'Videos':
                        videos_dir = os.sep.join(path_parts[:i+1])
                        self.current_project = os.sep.join(path_parts[:i-1])
                        break
                        
                if not videos_dir or not self.current_project:
                    self.log_message("无法确定视频所属项目，请先打开正确的项目")
                    return False
                    
                # Now populate the video list
                self.set_project(self.current_project)
        
        # Find the video item in the tree
        found_item = None
        for i in range(self.video_list.topLevelItemCount()):
            group_item = self.video_list.topLevelItem(i)
            for j in range(group_item.childCount()):
                child_item = group_item.child(j)
                item_path = child_item.data(0, Qt.UserRole)
                
                # Handle case where item_path is a tuple (path, prefix)
                if isinstance(item_path, tuple):
                    # Normalize paths by replacing separators for comparison
                    norm_item_path = item_path[0].replace('\\', '/')
                    norm_video_path = video_path.replace('\\', '/')
                    if norm_item_path == norm_video_path:
                        found_item = child_item
                        break
                # Handle case where item_path is a string
                elif item_path:
                    # Normalize paths by replacing separators for comparison
                    norm_item_path = item_path.replace('\\', '/')
                    norm_video_path = video_path.replace('\\', '/')
                    if norm_item_path == norm_video_path:
                        found_item = child_item
                        break
            if found_item:
                break
        
        if found_item:
            # Select the item in the tree - this will trigger on_video_selected
            self.video_list.setCurrentItem(found_item)
            # Also explicitly call the selection method to ensure it runs
            self.on_video_selected(found_item)
            self.log_message(f"已加载视频: {os.path.basename(video_path)}")
            return True
        else:
            self.log_message(f"在视频列表中未找到: {os.path.basename(video_path)}")
            return False

    def init_ui(self):
        """Initialize the user interface components"""
        # Main layout components
        self.meta_summary_button = QPushButton("无元数据")
        self.meta_summary_button.setStyleSheet("max-width:180px; text-align:left; padding:5px;")
        self.meta_summary_button.clicked.connect(self.show_full_meta_info)
        
        # Create control panel
        self.control_panel = self._create_control_panel()
        
        # Create image views panel
        self.images_panel = QSplitter(Qt.Vertical)
        self.rgb_view = AnnotatableView()
        self.ti_view = AnnotatableView()
        
        # 设置对AnnotationWidget的引用
        self.rgb_view.set_annotation_widget(self)
        self.ti_view.set_annotation_widget(self)
        
        self.rgb_view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.ti_view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        
        self.images_panel.addWidget(self.rgb_view)
        self.images_panel.addWidget(self.ti_view)
        self.images_panel.setSizes([self.images_panel.height() // 2, self.images_panel.height() // 2])
        
        # Create annotation options panel
        meta_container = self._create_annotation_panel()
        
        # Main splitter with three components
        main_splitter = QSplitter(Qt.Horizontal)
        main_splitter.addWidget(self.control_panel)
        main_splitter.addWidget(self.images_panel)
        main_splitter.addWidget(meta_container)
        main_splitter.setStretchFactor(1, 1)
        
        main_layout = QVBoxLayout()
        main_layout.addWidget(main_splitter)
        self.setLayout(main_layout)
        
        # Set default to single column mode (RGB only)
        self.ti_view.setVisible(False)
        self.toggle_modality_button.show()  # 确保模态切换按钮显示
        self.toggle_modality_button.setText("显示TI")
        self.toggle_layout_button.setText("切换为双栏显示")
        
        # Connect signals
        self.rgb_view.annotationChanged.connect(self.syncFromRGB)
        self.ti_view.annotationChanged.connect(self.syncFromTI)
        
        # Connect annotation button clicks to enable color selection
        self.annotation_button_group.buttonClicked.connect(self.on_annotation_selected)
        
        # Set stylesheet
        self.setStyleSheet("""
            QLabel { font-size: 14px; }
            QPushButton { padding: 5px; }
            QWidget { background-color: #f0f0f0; }
        """)
        
        # 修改：默认为原始分辨率模式
        self.autoFit = True
        self.rgb_view.autoFit = False
        self.ti_view.autoFit = False
        self.rgb_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.rgb_view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.ti_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.ti_view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)

        # 原始分辨率模式标志设为True
        self.show_full_resolution = True
        # 默认选中原始分辨率按钮
        self.full_resolution_button.setChecked(True)
        self.full_resolution_button.setText("适应视图")

        # 保存标注坐标映射信息
        self.scale_factor = 1.0
        self.offset_x = 0
        self.offset_y = 0

        # 添加快捷键开关和状态标记
        self.shortcuts_enabled = False
        self.shortcuts = []
        self.selected_annotation = None  # 当前选中的标注框

    def clear_selection_states(self):
        """清除所有选择状态，在预览跳转后调用以确保UI状态一致"""
        try:
            # 清除预览选择相关的状态
            if hasattr(self, '_preview_selected_annotation'):
                delattr(self, '_preview_selected_annotation')
                
            # 释放预览锁
            if hasattr(self, '_preview_lock'):
                self._preview_lock = False
                
            # 确保选择的标注项能被高亮显示但不影响正常标注
            if self.highlighted_item and hasattr(self, 'bbox_tree') and self.bbox_tree.selectedItems():
                # 如果有选中的标注，那么只禁用该颜色ID的标注
                item = self.bbox_tree.selectedItems()[0]
                ann = item.data(0, Qt.UserRole)
                if ann:
                    color_id = ann.get('color_id')
                    self.disable_annotation_for_color(color_id)
                else:
                    # 恢复正常的标注功能
                    selected = self.annotation_button_group.checkedId()
                    if selected != -1:
                        btn = self.annotation_button_group.checkedButton()
                        color = btn.styleSheet().split("color:")[1].split(";")[0].strip()
                        
                        if self.current_single_mode == "RGB":
                            self.rgb_view.set_annotation_enabled(True)
                            self.rgb_view.set_current_color(selected, color)
                        else:
                            self.ti_view.set_annotation_enabled(True)
                            self.ti_view.set_current_color(selected, color)
            
            self.log_message("预览选择状态已清除")
                        
        except Exception as e:
            self.log_message(f"清除选择状态时出错: {str(e)}")

    def clean_up_preview_state(self):
        """清理所有与预览相关的状态，防止切换视频时崩溃"""
        # 停止任何活跃的计时器
        if hasattr(self, '_preview_jump_timer') and self._preview_jump_timer and self._preview_jump_timer.isActive():
            self._preview_jump_timer.stop()
        
        # 重置所有锁和标志
        if hasattr(self, '_preview_lock'):
            self._preview_lock = False
            
        if hasattr(self, '_preview_selected_annotation'):
            delattr(self, '_preview_selected_annotation')
            
        # 取消选择
        if hasattr(self, 'bbox_tree'):
            self.bbox_tree.clearSelection()
            
        # 确保视图标注功能正确启用
        selected = self.annotation_button_group.checkedId()
        if selected != -1:
            btn = self.annotation_button_group.checkedButton()
            color_text = btn.styleSheet()
            color = color_text.split("color:")[1].split(";")[0].strip() if "color:" in color_text else "#FF0000"
            
            if self.current_single_mode == "RGB":
                self.rgb_view.set_annotation_enabled(True)
                self.rgb_view.set_current_color(selected, color)
                self.ti_view.set_annotation_enabled(False)
            else:
                self.ti_view.set_annotation_enabled(True)
                self.ti_view.set_current_color(selected, color)
                self.rgb_view.set_annotation_enabled(False)
  
    def _load_images(self, rgb_path, ti_path, force_reload=False):
        """Load images and update views"""
        self.rgb_view.scene().clear()
        self.ti_view.scene().clear()
        
        # 保存原图大小和经过处理后的图像大小，用于标注坐标映射
        self.original_rgb_size = None
        
        if (rgb_path and os.path.exists(rgb_path)):
            try:
                self.rgb = cv2.imread(rgb_path)
                if self.rgb is not None:
                    # 保存原始图像尺寸
                    self.original_rgb_size = (self.rgb.shape[1], self.rgb.shape[0])
                    
                    pixmap_rgb = None
                    
                    if self.show_full_resolution:
                        # 显示原始分辨率图像
                        pixmap_rgb = cv2_to_qpixmap(self.rgb)
                        # 不保存调整尺寸的副本，因为我们使用原始图像
                        self.rgb_resized = self.rgb.copy()
                        # 记录坐标映射信息 - 原始分辨率模式下无需缩放
                        self.scale_factor = 1.0
                        self.offset_x = 0
                        self.offset_y = 0
                    else:
                        # 标准流程，调整RGB尺寸以匹配TI尺寸
                        # 计算目标尺寸
                        if hasattr(self, 'ti') and self.ti is not None:
                            target_width, target_height = self.ti.shape[1], self.ti.shape[0]
                        else:
                            # 如果没有TI图像，则使用默认的目标尺寸
                            target_width, target_height = 640, 512  # 常见热成像分辨率
                        
                        # 调整大小并保存为rgb_resized
                        self.rgb_resized = center_crop_resize(self.rgb, target_width, target_height)
                        pixmap_rgb = cv2_to_qpixmap(self.rgb_resized)
                        
                        # 计算坐标映射因子
                        orig_w, orig_h = self.original_rgb_size
                        self.scale_factor = target_width / orig_w
                        
                        # 计算中心裁剪的偏移量
                        orig_aspect = orig_w / orig_h
                        target_aspect = target_width / target_height
                        
                        if orig_aspect > target_aspect:
                            # 原图更宽，上下会有裁剪
                            scaled_height = int(orig_h * (target_width / orig_w))
                            self.offset_y = (scaled_height - target_height) // 2
                            self.offset_x = 0
                        else:
                            # 原图更高，左右会有裁剪
                            scaled_width = int(orig_w * (target_height / orig_h))
                            self.offset_x = (scaled_width - target_width) // 2
                            self.offset_y = 0
                    
                    self.rgb_filename_edit.setText(os.path.basename(rgb_path))
                    self.rgb_filepath_edit.setText(rgb_path)
                    
                    # 更新场景
                    if pixmap_rgb is not None:
                        self.rgb_view.scene().addPixmap(pixmap_rgb)
                        self.rgb_view.scene().setSceneRect(QRectF(pixmap_rgb.rect()))
                else:
                    QMessageBox.warning(self, "错误", f"无法加载RGB图像: {rgb_path}")
            except Exception as e:
                logger.error(f"Error loading RGB image: {e}")
        else:
            self.rgb = None
            self.rgb_view.scene().clear()
            self.rgb_filename_edit.setText("N/A")
            self.rgb_filepath_edit.setText("N/A")
                
        # Load TI image if path provided - TI always loads at native resolution
        if (ti_path and os.path.exists(ti_path)):
            try:
                self.ti = cv2.imread(ti_path)
                if self.ti is None:
                    QMessageBox.warning(self, "错误", f"无法加载TI图像: {ti_path}")
                else:
                    pixmap_ti = cv2_to_qpixmap(self.ti)
                    self.ti_view.scene().clear()
                    self.ti_view.scene().addPixmap(pixmap_ti)
                    self.ti_view.scene().setSceneRect(QRectF(pixmap_ti.rect()))
                    self.ti_filename_edit.setText(os.path.basename(ti_path))
                    self.ti_filepath_edit.setText(ti_path)
            except Exception as e:
                logger.error(f"Error loading TI image: {e}")
        else:
            self.ti = None
            self.ti_view.scene().clear()
            self.ti_filename_edit.setText("N/A")
            self.ti_filepath_edit.setText("N/A")
                
        # Apply any offset and fit to view
        self.update_movement()
        
        # 根据当前模式决定是否自动适应视图
        if self.autoFit and not self.show_full_resolution:
            self._auto_fit_views()
        else:
            # 如果是原始尺寸或原始分辨率模式，确保滚动条可用
            self.rgb_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
            self.rgb_view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
            self.ti_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
            self.ti_view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        
        self.update_frame_metadata()
        
        # Important: Load annotations for the frame after loading the images
        self.load_frame_annotations()
        
        return True
        
    def _create_control_panel(self):
        """Create the left control panel with navigation controls and improved log display"""
        panel = QWidget()
        panel.setFixedWidth(250)
        layout = QVBoxLayout()
        
        # Video list
        layout.addWidget(QLabel("视频列表:"))
        self.video_list = QTreeWidget()
        self.video_list.setHeaderHidden(True)
        self.video_list.itemClicked.connect(self.on_video_selected)
        layout.addWidget(self.video_list)
        

        # Status label
        self.load_status_label = QLabel("未加载")
        layout.addWidget(self.load_status_label)
        
        # Navigation controls
        nav_layout = QHBoxLayout()
        self.prev_button = QPushButton("上一张")
        self.prev_button.clicked.connect(self.prev_frame)
        self.next_button = QPushButton("下一张")
        self.next_button.clicked.connect(self.next_frame)
        nav_layout.addWidget(self.prev_button)
        nav_layout.addWidget(self.next_button)
        layout.addLayout(nav_layout)
        
        # Zoom controls
        zoom_layout = QHBoxLayout()
        self.zoom_in_button = QPushButton("放大")
        self.zoom_in_button.clicked.connect(self.zoom_in)
        self.zoom_out_button = QPushButton("缩小")
        self.zoom_out_button.clicked.connect(self.zoom_out)
        zoom_layout.addWidget(self.zoom_in_button)
        zoom_layout.addWidget(self.zoom_out_button)
        layout.addLayout(zoom_layout)
        
        # 删除了1:1原始尺寸按钮
        
        # 添加原始分辨率按钮
        self.full_resolution_button = QPushButton("原始分辨率")
        self.full_resolution_button.setCheckable(True)
        self.full_resolution_button.clicked.connect(self.toggle_full_resolution)
        layout.addWidget(self.full_resolution_button)
        
        # 修改偏移滑块的标签和提示
        # offset_x_layout = QHBoxLayout()
        # offset_x_label = QLabel("X偏移(标注同步):")
        # offset_x_layout.addWidget(offset_x_label)
        # self.offset_slider_x = QSlider(Qt.Horizontal)
        # self.offset_slider_x.setRange(-50, 50)
        # self.offset_slider_x.setValue(0)
        # self.offset_slider_x.valueChanged.connect(lambda: self.on_offset_changed(0))
        # self.offset_slider_x.setToolTip("调整标注同步时的X轴偏移量")
        # offset_x_layout.addWidget(self.offset_slider_x)
        # layout.addLayout(offset_x_layout)
        
        # offset_y_layout = QHBoxLayout()
        # offset_y_label = QLabel("Y偏移(标注同步):")
        # offset_y_layout.addWidget(offset_y_label)
        # self.offset_slider_y = QSlider(Qt.Horizontal)
        # self.offset_slider_y.setRange(-50, 50)
        # self.offset_slider_y.setValue(0)
        # self.offset_slider_y.valueChanged.connect(lambda: self.on_offset_changed(0))
        # self.offset_slider_y.setToolTip("调整标注同步时的Y轴偏移量")
        # offset_y_layout.addWidget(self.offset_slider_y)
        # layout.addLayout(offset_y_layout)
        
        # Frame progress
        self.frame_slider = QSlider(Qt.Horizontal)
        self.frame_slider.setRange(0, 0)
        self.frame_slider.setValue(0)
        self.frame_slider.valueChanged.connect(self.on_frame_slider_changed)
        self.frame_label = QLabel("当前帧: 0/0")
        layout.addWidget(self.frame_slider)
        layout.addWidget(self.frame_label)
        
        # Jump to frame
        jump_layout = QHBoxLayout()
        self.jump_edit = QLineEdit()
        self.jump_edit.setPlaceholderText("帧号")
        self.jump_edit.setFixedWidth(50)
        self.jump_button = QPushButton("跳转")
        self.jump_button.clicked.connect(self.jump_to_frame)
        jump_layout.addWidget(self.jump_edit)
        jump_layout.addWidget(self.jump_button)
        layout.addLayout(jump_layout)
        
        # Playback control with speed selector
        playback_layout = QHBoxLayout()
        self.play_button = QPushButton("播放")
        self.play_button.setCheckable(True)
        self.play_button.clicked.connect(self.toggle_play)
        playback_layout.addWidget(self.play_button)
        
        # Add speed selector combo box
        self.speed_label = QLabel("速度:")
        self.speed_selector = QComboBox()
        self.speed_selector.addItems(["0.25x", "0.5x", "1x", "2x", "4x"])
        self.speed_selector.setCurrentText("1x")
        self.speed_selector.currentTextChanged.connect(self.on_speed_changed)
        playback_layout.addWidget(self.speed_label)
        playback_layout.addWidget(self.speed_selector)
        
        layout.addLayout(playback_layout)
        
        # Layout toggle buttons
        toggle_buttons_layout = QHBoxLayout()
        self.toggle_layout_button = QPushButton("切换为单栏显示")
        self.toggle_layout_button.clicked.connect(self.toggleLayout)
        self.toggle_modality_button = QPushButton("显示TI")
        self.toggle_modality_button.clicked.connect(self.toggleModality)
        toggle_buttons_layout.addWidget(self.toggle_layout_button)
        toggle_buttons_layout.addWidget(self.toggle_modality_button)
        layout.addLayout(toggle_buttons_layout)
        
        self.clear_button = QPushButton("清除标注")
        self.clear_button.clicked.connect(self.clearAnnotations)
        layout.addWidget(self.clear_button)
        
        self.info_label = QPlainTextEdit()
        self.info_label.setReadOnly(True)
        self.info_label.setFixedHeight(80)  # Make it taller to show more lines
        self.info_label.setStyleSheet("background-color: #f8f8f8; border: 1px solid #ddd;")
        layout.addWidget(self.info_label)
        
        # Add stretch to push following widgets to the bottom
        layout.addStretch(1)
        
        # File info
        self.rgb_filename_edit = QLineEdit()
        self.rgb_filename_edit.setReadOnly(True)
        self.ti_filename_edit = QLineEdit()
        self.ti_filename_edit.setReadOnly(True)
        self.rgb_filepath_edit = QLineEdit()
        self.rgb_filepath_edit.setReadOnly(True)
        self.ti_filepath_edit = QLineEdit()
        self.ti_filepath_edit.setReadOnly(True)
        
        layout.addWidget(QLabel("RGB图文件名:"))
        layout.addWidget(self.rgb_filename_edit)
        layout.addWidget(QLabel("TI图文件名:"))
        layout.addWidget(self.ti_filename_edit)
        layout.addWidget(QLabel("RGB图路径:"))
        layout.addWidget(self.rgb_filepath_edit)
        layout.addWidget(QLabel("TI图路径:"))
        layout.addWidget(self.ti_filepath_edit)
        
        # 替换单个同时标注复选框，改为更完整的标注选项组
        annotation_options_group = QGroupBox("标注选项")
        annotation_options_layout = QVBoxLayout()
        
        # 同时标注选项 (已有)
        self.simul_checkbox = QCheckBox("同时标注")
        self.simul_checkbox.setChecked(False)
        self.simul_checkbox.stateChanged.connect(self.toggle_simultaneous_annotation)
        annotation_options_layout.addWidget(self.simul_checkbox)
        
        # 添加新选项：标注框尺寸同步
        self.sync_bbox_size = QCheckBox("同步标注框尺寸")
        self.sync_bbox_size.setChecked(False)
        self.sync_bbox_size.setToolTip("启用后，RGB和TI视图的标注框将保持相同尺寸")
        self.sync_bbox_size.stateChanged.connect(self.on_sync_bbox_size_changed)
        annotation_options_layout.addWidget(self.sync_bbox_size)
        
        # Add selection mode checkbox
        self.selection_mode_checkbox = QCheckBox("选择模式")
        self.selection_mode_checkbox.setChecked(False)
        self.selection_mode_checkbox.stateChanged.connect(self.toggle_selection_mode)
        annotation_options_layout.addWidget(self.selection_mode_checkbox)
        
        annotation_options_group.setLayout(annotation_options_layout)
        layout.addWidget(annotation_options_group)
        
        layout.addStretch(1)
        panel.setLayout(layout)
        return panel

    def toggle_selection_mode(self, state):
        """Toggle selection mode for annotations - when enabled, can select annotations without drawing new ones"""
        is_enabled = state == Qt.Checked
        
        # First, save any pending modifications
        if self.annotation_modified:
            self.save_annotations_to_disk()
            
        # Clear any existing highlight
        if self.highlighted_item and not sip.isdeleted(self.highlighted_item):
            try:
                scene = self.highlighted_item.scene()
                if scene:
                    scene.removeItem(self.highlighted_item)
            except (RuntimeError, TypeError, Exception):
                pass
        self.highlighted_item = None
        
        # Clear tree selection to avoid state inconsistency
        if hasattr(self, 'bbox_tree'):
            self.bbox_tree.clearSelection()
        
        # Update views
        if hasattr(self.rgb_view, 'set_selection_mode'):
            self.rgb_view.set_selection_mode(is_enabled)
        if hasattr(self.ti_view, 'set_selection_mode'):
            self.ti_view.set_selection_mode(is_enabled)
        
        # If selection mode is enabled, disable drawing temporarily
        if is_enabled:
            # Remember current annotation state to restore it later
            self._prev_annotation_enabled = {
                "RGB": self.rgb_view.annotation_enabled if hasattr(self.rgb_view, 'annotation_enabled') else False,
                "TI": self.ti_view.annotation_enabled if hasattr(self.ti_view, 'annotation_enabled') else False,
                "color_id": self.annotation_button_group.checkedId(),
            }
            
            # Disable annotation drawing
            self.rgb_view.set_annotation_enabled(False)
            self.ti_view.set_annotation_enabled(False)
            self.log_message("已启用选择模式 - 可以选择标注框但不会绘制新标注")
        else:
            # Restore previous annotation state
            if hasattr(self, '_prev_annotation_enabled'):
                selected = self._prev_annotation_enabled.get("color_id", -1)
                if selected != -1:
                    btn = self.annotation_button_group.button(selected)
                    if btn:
                        btn.setChecked(True)
                        color = self.color_map.get(selected, "#FF0000")
                        
                        # Restore annotation state
                        if self.two_column_mode:
                            self.rgb_view.set_annotation_enabled(True)
                            self.ti_view.set_annotation_enabled(True)
                            self.rgb_view.set_current_color(selected, color)
                            self.ti_view.set_current_color(selected, color)
                        else:
                            if self.current_single_mode == "RGB":
                                self.rgb_view.set_annotation_enabled(
                                    self._prev_annotation_enabled.get("RGB", False))
                                self.rgb_view.set_current_color(selected, color)
                            else:
                                self.ti_view.set_annotation_enabled(
                                    self._prev_annotation_enabled.get("TI", False))
                                self.ti_view.set_current_color(selected, color)
                
                self.log_message("已禁用选择模式 - 恢复正常标注功能")

            # Refresh all annotations to ensure proper rendering
            self.redraw_all_annotations()

    # 然后添加这个新方法:
    def on_sync_bbox_size_changed(self, state):
        """处理同步标注框尺寸状态变化"""
        is_checked = (state == Qt.Checked)
        
        # 如果启用同步框尺寸，则禁用同时标注功能
        if is_checked and self.simultaneous_annotation:
            self.simul_checkbox.blockSignals(True)
            self.simul_checkbox.setChecked(False)
            self.simul_checkbox.blockSignals(False)
            self.simultaneous_annotation = False
            self.log_message("已禁用同时标注功能（与同步标注框尺寸冲突）")
        
        if is_checked:
            self.log_message("已启用同步标注框尺寸 - RGB和TI标注框将保持相同尺寸")
        else:
            self.log_message("已禁用同步标注框尺寸")

  
    def jump_to_frame(self):
        """Jump to a specific frame number entered by the user"""
        try:
            # Get entered frame number (1-based) and convert to 0-based index
            frame_number = int(self.jump_edit.text())
            frame_index = frame_number - 1  # Convert to 0-based index
            
            # Check if we have frames loaded and the frame number is valid
            total_frames = 0
            if hasattr(self, "rgb_frames") and self.rgb_frames:
                total_frames = len(self.rgb_frames)
            elif hasattr(self, "ti_frames") and self.ti_frames:
                total_frames = len(self.ti_frames)
            
            if total_frames == 0:
                self.log_message("无帧可加载")
                return
                
            # Validate frame index is in range
            if frame_index < 0 or frame_index >= total_frames:
                self.log_message(f"帧号超出范围: 有效范围 1-{total_frames}")
                return
                
            # Auto-save current frame annotations before jumping
            if hasattr(self, 'current_frame_index') and self.current_frame_index != frame_index:
                self.auto_save_annotations()
            
            # Set the current frame index
            self.current_frame_index = frame_index
            
            # Update slider position
            self.frame_slider.blockSignals(True)  # Block signals to prevent recursion
            self.frame_slider.setValue(frame_index)
            self.frame_slider.blockSignals(False)
            
            # Load the new frame and its annotations
            if hasattr(self, "rgb_frames") and self.rgb_frames:
                ti_path = self.ti_frames[frame_index] if self.ti_frames and len(self.ti_frames) > frame_index else None
                self.load_current_frame_with_annotations(self.rgb_frames[frame_index], ti_path)
                total = len(self.rgb_frames)
            elif hasattr(self, "ti_frames") and self.ti_frames:
                rgb_path = self.rgb_frames[frame_index] if self.rgb_frames and len(self.rgb_frames) > frame_index else None
                self.load_current_frame_with_annotations(rgb_path, self.ti_frames[frame_index])
                total = len(self.ti_frames)
            
            # Update frame counter
            self.frame_label.setText(f"当前帧: {frame_number}/{total}")
            self.log_message(f"已跳转至帧 {frame_number}")
            
        except ValueError:
            self.log_message("请输入有效的帧号")

    def load_current_frame_with_annotations(self, rgb_path, ti_path):
        """Loads a frame and its annotations with proper clearing and redrawing"""
        # Block signals during cleanup to prevent unintended callbacks
        if hasattr(self, 'rgb_view') and self.rgb_view and hasattr(self.rgb_view, 'scene'):
            self.rgb_view.scene().blockSignals(True)
        if hasattr(self, 'ti_view') and self.ti_view and hasattr(self.ti_view, 'scene'):
            self.ti_view.scene().blockSignals(True)
        
        # First clear existing annotations
        self.annotations = []
        self.rgb_view.clearAnnotations()
        self.ti_view.clearAnnotations()
        
        # Clear any highlight
        if self.highlighted_item and not sip.isdeleted(self.highlighted_item):
            try:
                scene = self.highlighted_item.scene()
                if scene:
                    scene.removeItem(self.highlighted_item)
            except RuntimeError:
                pass
        self.highlighted_item = None
        
        # Re-enable signals
        if hasattr(self, 'rgb_view') and self.rgb_view and hasattr(self.rgb_view, 'scene'):
            self.rgb_view.scene().blockSignals(False)
        if hasattr(self, 'ti_view') and self.ti_view and hasattr(self.ti_view, 'scene'):
            self.ti_view.scene().blockSignals(False)
        
        # Load the images
        if rgb_path or ti_path:
            self.load_images(rgb_path, ti_path)
        
        # The load_images function should call load_frame_annotations, but let's be sure
        self.load_frame_annotations()
        self.update_bbox_tree()
        
        # Force garbage collection to clean up any orphaned graphics items
        import gc
        gc.collect()

    def _create_annotation_panel(self):
        """创建右侧标注面板，合并编辑工具和标注控制"""
        container = QWidget()
        layout = QVBoxLayout()
        layout.addWidget(self.meta_summary_button)
        
        # 添加偏移控制面板到右侧面板顶部
        offset_group = QGroupBox("标注同步偏移控制")
        offset_layout = QGridLayout()
        
        # X偏移控制 - 增加范围并添加手动输入框
        offset_layout.addWidget(QLabel("X偏移:"), 0, 0)
        self.offset_slider_x = QSlider(Qt.Horizontal)
        self.offset_slider_x.setRange(-100, 100)  # 增加范围到±100
        self.offset_slider_x.setValue(0)
        self.offset_slider_x.valueChanged.connect(self.on_offset_slider_changed)
        self.offset_slider_x.setToolTip("调整标注同步时的X轴偏移量")
        offset_layout.addWidget(self.offset_slider_x, 0, 1)
        
        self.offset_x_edit = QSpinBox()
        self.offset_x_edit.setRange(-100, 100)  # 与滑块范围一致
        self.offset_x_edit.setValue(0)
        self.offset_x_edit.valueChanged.connect(self.on_offset_edit_changed)
        offset_layout.addWidget(self.offset_x_edit, 0, 2)
        
        # Y偏移控制 - 增加范围并添加手动输入框
        offset_layout.addWidget(QLabel("Y偏移:"), 1, 0)
        self.offset_slider_y = QSlider(Qt.Horizontal)
        self.offset_slider_y.setRange(-100, 100)  # 增加范围到±100
        self.offset_slider_y.setValue(0)
        self.offset_slider_y.valueChanged.connect(self.on_offset_slider_changed)
        self.offset_slider_y.setToolTip("调整标注同步时的Y轴偏移量")
        offset_layout.addWidget(self.offset_slider_y, 1, 1)
        
        self.offset_y_edit = QSpinBox()
        self.offset_y_edit.setRange(-100, 100)  # 与滑块范围一致
        self.offset_y_edit.setValue(0)
        self.offset_y_edit.valueChanged.connect(self.on_offset_edit_changed)
        offset_layout.addWidget(self.offset_y_edit, 1, 2)
        
        # 添加应用按钮
        apply_offset_btn = QPushButton("应用偏移")
        apply_offset_btn.clicked.connect(self.apply_current_offset)
        apply_offset_btn.setToolTip("应用当前偏移值到选中的标注")
        offset_layout.addWidget(apply_offset_btn, 2, 0, 1, 3)
        
        offset_group.setLayout(offset_layout)
        layout.addWidget(offset_group)
        
        # 使用TabWidget来组织右侧面板，解决空间不足问题
        tab_widget = QTabWidget()
        
        # 标签页1: 标注与编辑控制(合并后)
        annotation_tab = QWidget()
        annotation_layout = QVBoxLayout(annotation_tab)
        
        # 快捷键控制组 - 简化为单个按钮
        shortcuts_layout = QHBoxLayout()
        self.shortcuts_toggle = QPushButton("启用快捷键")
        self.shortcuts_toggle.setCheckable(True)
        self.shortcuts_toggle.toggled.connect(self.toggle_shortcuts)
        shortcuts_layout.addWidget(self.shortcuts_toggle)
        
        # 添加一个问号图标按钮，点击显示快捷键帮助
        help_btn = QPushButton("?")
        help_btn.setFixedWidth(25)
        help_btn.setToolTip("查看快捷键帮助")
        help_btn.clicked.connect(self.show_shortcuts_help)
        shortcuts_layout.addWidget(help_btn)
        

        
        # 添加简洁的状态指示器
        self.shortcut_status = QLabel("已禁用")
        self.shortcut_status.setStyleSheet("color: gray;")
        shortcuts_layout.addWidget(self.shortcut_status)
        
        shortcuts_layout.addStretch()
        annotation_layout.addLayout(shortcuts_layout)
        
        # 颜色选择组
        self.annotation_option_box = QGroupBox("标注选项")
        option_layout = QGridLayout()  # 使用网格布局，每行放置2个选项
        self.annotation_button_group = QButtonGroup(self)
        row, col = 0, 0
        for opt_id, (color_id, color) in enumerate(DEFAULT_COLORS.items(), 1):
            name = self._get_color_name(color_id)
            rb = QRadioButton(f"ID{color_id}")
            rb.setStyleSheet(f"color: {color};")
            self.annotation_button_group.addButton(rb, color_id)
            option_layout.addWidget(rb, row, col)
            col += 1
            if col > 1:  # 每行2列
                col = 0
                row += 1
        self.annotation_option_box.setLayout(option_layout)
        annotation_layout.addWidget(self.annotation_option_box)
        
        # 标注框编辑组（原"编辑工具"标签页内容）
        edit_group = QGroupBox("标注框编辑")
        self.edit_group = edit_group  # 保存引用以便其他方法使用
        edit_controls = QGridLayout()
        
        # X坐标和Y坐标
        edit_controls.addWidget(QLabel("X:"), 0, 0)
        self.edit_x = QSpinBox()
        self.edit_x.setRange(0, 4000)
        self.edit_x.setSingleStep(1)
        self.edit_x.valueChanged.connect(self.update_annotation_position)
        self.edit_x.setEnabled(False)
        edit_controls.addWidget(self.edit_x, 0, 1)
        
        edit_controls.addWidget(QLabel("Y:"), 0, 2)
        self.edit_y = QSpinBox()
        self.edit_y.setRange(0, 4000)
        self.edit_y.setSingleStep(1)
        self.edit_y.valueChanged.connect(self.update_annotation_position)
        self.edit_y.setEnabled(False)
        edit_controls.addWidget(self.edit_y, 0, 3)
        
        # 宽度和高度
        edit_controls.addWidget(QLabel("宽:"), 1, 0)
        self.edit_w = QSpinBox()
        self.edit_w.setRange(1, 4000)
        self.edit_w.setSingleStep(1)
        self.edit_w.valueChanged.connect(self.update_annotation_size)
        self.edit_w.setEnabled(False)
        edit_controls.addWidget(self.edit_w, 1, 1)
        
        edit_controls.addWidget(QLabel("高:"), 1, 2)
        self.edit_h = QSpinBox()
        self.edit_h.setRange(1, 4000)
        self.edit_h.setSingleStep(1)
        self.edit_h.valueChanged.connect(self.update_annotation_size)
        self.edit_h.setEnabled(False)
        edit_controls.addWidget(self.edit_h, 1, 3)
        
        # 调整步长和应用按钮
        edit_controls.addWidget(QLabel("步长:"), 2, 0)
        self.adjust_step = QSpinBox()
        self.adjust_step.setRange(1, 10)
        self.adjust_step.setValue(1)
        edit_controls.addWidget(self.adjust_step, 2, 1)
        
        self.apply_edit_btn = QPushButton("应用修改")
        self.apply_edit_btn.clicked.connect(self.apply_annotation_edit)
        self.apply_edit_btn.setEnabled(False)
        edit_controls.addWidget(self.apply_edit_btn, 2, 2, 1, 2)
        
        # 微调按钮
        adjustment_layout = QGridLayout()
        self.adjustment_layout = adjustment_layout  # 保存引用
        
        # 为了节省空间，将按钮做得更小并使用图标
        x_dec_btn = QPushButton("←")
        x_dec_btn.setFixedSize(30, 20)
        x_dec_btn.clicked.connect(lambda: self.adjust_annotation_position(-1, 0))
        adjustment_layout.addWidget(x_dec_btn, 0, 0)
        
        y_dec_btn = QPushButton("↑")
        y_dec_btn.setFixedSize(30, 20)
        y_dec_btn.clicked.connect(lambda: self.adjust_annotation_position(0, -1))
        adjustment_layout.addWidget(y_dec_btn, 0, 1)
        
        y_inc_btn = QPushButton("↓")
        y_inc_btn.setFixedSize(30, 20)
        y_inc_btn.clicked.connect(lambda: self.adjust_annotation_position(0, 1))
        adjustment_layout.addWidget(y_inc_btn, 0, 2)
        
        x_inc_btn = QPushButton("→")
        x_inc_btn.setFixedSize(30, 20)
        x_inc_btn.clicked.connect(lambda: self.adjust_annotation_position(1, 0))
        adjustment_layout.addWidget(x_inc_btn, 0, 3)
        
        # 大小调整按钮放在一行
        w_dec_btn = QPushButton("W-")
        w_dec_btn.setFixedSize(30, 20)
        w_dec_btn.clicked.connect(lambda: self.adjust_annotation_size(-1, 0))
        adjustment_layout.addWidget(w_dec_btn, 1, 0)
        
        h_dec_btn = QPushButton("H-")
        h_dec_btn.setFixedSize(30, 20)
        h_dec_btn.clicked.connect(lambda: self.adjust_annotation_size(0, -1))
        adjustment_layout.addWidget(h_dec_btn, 1, 1)
        
        h_inc_btn = QPushButton("H+")
        h_inc_btn.setFixedSize(30, 20)
        h_inc_btn.clicked.connect(lambda: self.adjust_annotation_size(0, 1))
        adjustment_layout.addWidget(h_inc_btn, 1, 2)
        
        w_inc_btn = QPushButton("W+")
        w_inc_btn.setFixedSize(30, 20)
        w_inc_btn.clicked.connect(lambda: self.adjust_annotation_size(1, 0))
        adjustment_layout.addWidget(w_inc_btn, 1, 3)
        
        # 禁用所有微调按钮
        for i in range(adjustment_layout.count()):
            widget = adjustment_layout.itemAt(i).widget()
            if isinstance(widget, QPushButton):
                widget.setEnabled(False)
                
        edit_controls.addLayout(adjustment_layout, 3, 0, 1, 4)
        edit_group.setLayout(edit_controls)
        annotation_layout.addWidget(edit_group)
        
        # 按钮面板
        button_panel = QHBoxLayout()
        self.deselect_button = QPushButton("取消选择")
        self.deselect_button.clicked.connect(self.deselect_annotation)
        button_panel.addWidget(self.deselect_button)
        
        self.save_button = QPushButton("保存标注")
        self.save_button.clicked.connect(self.on_save_annotations)
        button_panel.addWidget(self.save_button)
        annotation_layout.addLayout(button_panel)
        
        # 添加跟踪按钮
        self.track_button = QPushButton("跟踪标注")
        self.track_button.setToolTip("跟踪选中的标注框在后续帧中的位置")
        self.track_button.clicked.connect(self.show_track_dialog)
        button_panel.addWidget(self.track_button)
        
        # 标注预览按钮
        self.preview_annotations_button = QPushButton("视频标注预览")
        self.preview_annotations_button.clicked.connect(self.show_video_annotations_preview)
        annotation_layout.addWidget(self.preview_annotations_button)
        
        # 标注项列表
        label_tab = QWidget()
        label_layout = QVBoxLayout(label_tab)
        
        label_layout.addWidget(QLabel("标注信息:"))
        self.bbox_tree = QTreeWidget()
        self.bbox_tree.setHeaderLabels(["ID", "位置", "尺寸", "模态"])
        self.bbox_tree.setColumnWidth(0, 30)
        self.bbox_tree.setMinimumHeight(200)
        self.bbox_tree.itemSelectionChanged.connect(self.on_annotation_selected_in_tree)
        self.bbox_tree.itemDoubleClicked.connect(lambda item, col: self.show_annotation_details())
        label_layout.addWidget(self.bbox_tree)
        
        # 在标注列表标签页添加和标注控制页相同的按钮
        list_button_panel = QHBoxLayout()
        
        # 删除按钮
        self.delete_annotation_btn = QPushButton("删除选中标注")
        self.delete_annotation_btn.clicked.connect(self.delete_selected_annotation)
        list_button_panel.addWidget(self.delete_annotation_btn)
        
        # 添加取消选择按钮
        deselect_button_copy = QPushButton("取消选择")
        deselect_button_copy.clicked.connect(self.deselect_annotation)
        list_button_panel.addWidget(deselect_button_copy)
        
        label_layout.addLayout(list_button_panel)
        
        # 添加保存和预览按钮
        list_actions = QHBoxLayout()
        
        save_button_copy = QPushButton("保存标注")
        save_button_copy.clicked.connect(self.on_save_annotations)
        list_actions.addWidget(save_button_copy)
        
        preview_button_copy = QPushButton("视频标注预览")
        preview_button_copy.clicked.connect(self.show_video_annotations_preview)
        list_actions.addWidget(preview_button_copy)
        
        label_layout.addLayout(list_actions)
        
        # 添加标注选项组
        annotation_options_group = QGroupBox("标注选项")
        annotation_options_layout = QVBoxLayout()
        
        consistency_check_btn = QPushButton("标注一致性检查")
        consistency_check_btn.clicked.connect(self.check_annotation_consistency)
        consistency_check_btn.setToolTip("检查RGB和TI标注的一致性")
        annotation_options_layout.addWidget(consistency_check_btn)
        
        # 添加新的全局一致性检查按钮
        all_frames_consistency_btn = QPushButton("检查所有帧一致性")
        all_frames_consistency_btn.clicked.connect(self.check_all_frames_annotation_consistency)
        all_frames_consistency_btn.setToolTip("检查所有帧的RGB和TI标注一致性")
        annotation_options_layout.addWidget(all_frames_consistency_btn)
        
        # 同时标注选项
        self.simul_checkbox = QCheckBox("同时标注")
        self.simul_checkbox.setChecked(False)
        self.simul_checkbox.stateChanged.connect(self.toggle_simultaneous_annotation)
        annotation_options_layout.addWidget(self.simul_checkbox)
        
        # 添加新选项：标注框尺寸同步
        self.sync_bbox_size = QCheckBox("同步标注框尺寸")
        self.sync_bbox_size.setChecked(False)
        self.sync_bbox_size.setToolTip("启用后，RGB和TI视图的标注框将保持相同尺寸")
        self.sync_bbox_size.stateChanged.connect(self.on_sync_bbox_size_changed)
        annotation_options_layout.addWidget(self.sync_bbox_size)
        
        # 添加按ID删除按钮
        delete_by_id_btn = QPushButton("按ID删除")
        delete_by_id_btn.setToolTip("删除指定ID的所有标注")
        delete_by_id_btn.clicked.connect(self.show_delete_annotation_by_color_dialog)
        annotation_options_layout.addWidget(delete_by_id_btn)
        
        annotation_options_group.setLayout(annotation_options_layout)
        label_layout.addWidget(annotation_options_group)
        
        # 添加标签页到TabWidget - 现在只有两个标签页
        tab_widget.addTab(annotation_tab, "标注与编辑")
        tab_widget.addTab(label_tab, "标注列表")
        
        layout.addWidget(tab_widget)
        container.setLayout(layout)
        return container

    def on_offset_slider_changed(self, value):
        """处理偏移滑块变化"""
        # 阻止循环触发
        sender = self.sender()
        if sender == self.offset_slider_x:
            self.offset_x_edit.blockSignals(True)
            self.offset_x_edit.setValue(value)
            self.offset_x_edit.blockSignals(False)
        elif sender == self.offset_slider_y:
            self.offset_y_edit.blockSignals(True)
            self.offset_y_edit.setValue(value)
            self.offset_y_edit.blockSignals(False)
        
        # 原有的偏移处理逻辑
        x = self.offset_slider_x.value()
        y = self.offset_slider_y.value()
        self.offsetChanged.emit(x, y)
        
        # 不再调用update_movement()移动画面，而是更新偏移状态
        self.log_message(f"标注同步偏移已设置：X={x}, Y={y}")
        
        # 如果当前选中了标注且同时标注模式开启，立即应用偏移更新两个模态的标注
        if self.simultaneous_annotation and hasattr(self, 'selected_annotation') and self.selected_annotation:
            self.apply_offset_to_selected_annotation()
        elif self.simultaneous_annotation:
            # 如果同时标注模式开启但没有选中标注，只更新用于后续标注的偏移值
            self.log_message("偏移设置已保存，将用于后续同时标注")

    def on_offset_edit_changed(self, value):
        """处理偏移编辑框的值变化"""
        # 阻止循环触发
        sender = self.sender()
        if sender == self.offset_x_edit:
            self.offset_slider_x.blockSignals(True)
            self.offset_slider_x.setValue(value)
            self.offset_slider_x.blockSignals(False)
        elif sender == self.offset_y_edit:
            self.offset_slider_y.blockSignals(True)
            self.offset_slider_y.setValue(value)
            self.offset_slider_y.blockSignals(False)
        
        # 获取最新偏移值并发出信号
        x = self.offset_x_edit.value()
        y = self.offset_y_edit.value()
        self.offsetChanged.emit(x, y)
        
        self.log_message(f"标注同步偏移已设置：X={x}, Y={y}")

    def apply_current_offset(self):
        """手动应用当前偏移到选中标注"""
        selected_items = self.bbox_tree.selectedItems()
        if not selected_items:
            self.log_message("未选择标注，无法应用偏移")
            return
        
        # 获取选中的标注并应用偏移
        item = selected_items[0]
        ann = item.data(0, Qt.UserRole)
        if ann:
            modality = ann.get("modality")
            self.log_message(f"正在应用偏移到选中的{modality}标注...")
            self.apply_offset_to_selected_annotation()
        else:
            self.log_message("无效的标注数据，无法应用偏移")

    def apply_offset_to_selected_annotation(self):
        """应用当前偏移到选中的标注上，更新对应的同步标注"""
        # 获取当前选中的标注
        selected_items = self.bbox_tree.selectedItems()
        if not selected_items:
            return
        
        item = selected_items[0]
        ann = item.data(0, Qt.UserRole)
        if not ann:
            return
        
        # 获取选中标注的基本信息
        frame = ann.get("frame")
        color_id = ann.get("color_id")
        modality = ann.get("modality")
        bbox = ann.get("bbox")
        
        # 查找对应模态的标注
        target_modality = "TI" if modality == "RGB" else "RGB"
        target_ann = None
        
        for a in self.annotations:
            if (a.get("frame") == frame and 
                a.get("color_id") == color_id and 
                a.get("modality") == target_modality):
                target_ann = a
                break
        
        # 如果找不到对应的标注，无法应用偏移
        if not target_ann:
            self.log_message(f"无法找到对应的{target_modality}标注来应用偏移")
            return
        
        # 获取当前偏移值
        offset_x = self.offset_slider_x.value()
        offset_y = self.offset_slider_y.value()
        
        # 应用偏移值更新对应标注位置
        source_rect = QRectF(*bbox)
        
        # 使用修改后的transform_coordinates_between_modalities方法转换坐标
        target_rect = self.transform_coordinates_between_modalities(
            source_rect, modality, target_modality)
        
        # 更新目标标注的bbox
        target_ann["bbox"] = [target_rect.x(), target_rect.y(), 
                            target_rect.width(), target_rect.height()]
        
        # 更新显示
        self.redraw_all_annotations()
        self.update_bbox_tree()
        
        # 标记为已修改
        self.annotation_modified = True
        
        # 更新缓存
        if self.cached_video_id:
            frame_annotations = [ann for ann in self.annotations if ann.get("frame") == self.current_frame_index]
            self.annotation_cache[self.current_frame_index] = frame_annotations.copy()
        
        self.log_message(f"已应用{modality}到{target_modality}的标注偏移 (X:{offset_x}, Y:{offset_y})")

    def check_all_frames_annotation_consistency(self):
        """检查所有帧的RGB和TI标注的一致性，报告详细的不匹配信息，但不提供修复选项"""
        if not self.annotations:
            self.log_message("无标注可供检查")
            return
        
        # 显示进度对话框
        progress = QDialog(self)
        progress.setWindowTitle("正在检查所有帧...")
        progress_layout = QVBoxLayout(progress)
        progress_label = QLabel("正在收集所有帧的标注信息...")
        progress_layout.addWidget(progress_label)
        progress_bar = QProgressBar()
        progress_bar.setRange(0, 0)  # 不确定模式
        progress_layout.addWidget(progress_bar)
        progress.resize(300, 100)
        progress.show()
        QApplication.processEvents()
        
        try:
            # 按帧号和ID分组所有标注
            frame_id_annotations = {}
            
            # 首先收集所有标注信息，按帧和ID组织
            for ann in self.annotations:
                frame = ann.get("frame")
                color_id = ann.get("color_id")
                modality = ann.get("modality")
                
                key = (frame, color_id)
                if key not in frame_id_annotations:
                    frame_id_annotations[key] = {"RGB": None, "TI": None}
                
                frame_id_annotations[key][modality] = ann
            
            # 从缓存中也收集标注
            if hasattr(self, 'annotation_cache') and self.annotation_cache:
                for frame_idx, frame_annotations in self.annotation_cache.items():
                    for ann in frame_annotations:
                        frame = ann.get("frame")
                        color_id = ann.get("color_id")
                        modality = ann.get("modality")
                        
                        key = (frame, color_id)
                        if key not in frame_id_annotations:
                            frame_id_annotations[key] = {"RGB": None, "TI": None}
                        
                        # 只在尚未有此模态标注时更新
                        if not frame_id_annotations[key][modality]:
                            frame_id_annotations[key][modality] = ann
            
            # 更新进度信息
            progress_label.setText(f"正在分析 {len(frame_id_annotations)} 个标注项目...")
            QApplication.processEvents()
            
            # 检查不匹配情况
            mismatches = []
            
            for (frame, color_id), modalities in frame_id_annotations.items():
                rgb_ann = modalities["RGB"]
                ti_ann = modalities["TI"]
                
                if rgb_ann and ti_ann:
                    # 两个模态都有标注，检查尺寸是否匹配
                    rgb_bbox = rgb_ann.get("bbox", [0, 0, 0, 0])
                    ti_bbox = ti_ann.get("bbox", [0, 0, 0, 0])
                    
                    # 如果是原始分辨率模式，需要考虑到比例转换
                    rgb_width, rgb_height = rgb_bbox[2], rgb_bbox[3]
                    ti_width, ti_height = ti_bbox[2], ti_bbox[3]
                    
                    # 允许一定的误差（10%）
                    width_diff = abs(rgb_width - ti_width) / max(rgb_width, ti_width) if max(rgb_width, ti_width) > 0 else 0
                    height_diff = abs(rgb_height - ti_height) / max(rgb_height, ti_height) if max(rgb_height, ti_height) > 0 else 0
                    
                    if width_diff > 0.10 or height_diff > 0.10:
                        mismatches.append({
                            "type": "size_mismatch",
                            "frame": frame,
                            "color_id": color_id,
                            "rgb_size": (rgb_width, rgb_height),
                            "ti_size": (ti_width, ti_height)
                        })
                elif rgb_ann and not ti_ann:
                    # 只有RGB标注
                    mismatches.append({
                        "type": "missing_ti",
                        "frame": frame,
                        "color_id": color_id
                    })
                elif ti_ann and not rgb_ann:
                    # 只有TI标注
                    mismatches.append({
                        "type": "missing_rgb",
                        "frame": frame,
                        "color_id": color_id
                    })
            
            # 关闭进度对话框
            progress.close()
            
            # 显示结果
            if not mismatches:
                self.log_message("所有帧的标注一致性检查通过!")
                QMessageBox.information(self, "一致性检查", "所有帧的标注一致性检查通过!")
                return True
            
            # 按照帧号对不匹配进行排序
            mismatches.sort(key=lambda x: (x["frame"], x["color_id"]))
            
            # 创建详细报告
            report = []
            report.append("=== 所有帧标注一致性检查报告 ===")
            
            # 统计总共有多少帧有问题
            problem_frames = set(m["frame"] for m in mismatches)
            
            # 按类型分组不匹配
            missing_ti = [m for m in mismatches if m["type"] == "missing_ti"]
            missing_rgb = [m for m in mismatches if m["type"] == "missing_rgb"]
            size_mismatch = [m for m in mismatches if m["type"] == "size_mismatch"]
            
            report.append(f"\n共发现 {len(problem_frames)} 个帧有问题, 总计 {len(mismatches)} 个问题:")
            report.append(f"- 尺寸不匹配: {len(size_mismatch)} 个")
            report.append(f"- 缺少TI标注: {len(missing_ti)} 个")
            report.append(f"- 缺少RGB标注: {len(missing_rgb)} 个\n")
            
            if missing_ti:
                report.append("\n▶ 缺少TI标注:")
                for m in missing_ti[:30]:  # 限制显示数量以避免报告过长
                    report.append(f"  帧 {m['frame']+1}, ID {m['color_id']}: 有RGB标注但缺少TI标注")
                if len(missing_ti) > 30:
                    report.append(f"  ...以及其他 {len(missing_ti)-30} 个类似问题")
            
            if missing_rgb:
                report.append("\n▶ 缺少RGB标注:")
                for m in missing_rgb[:30]:
                    report.append(f"  帧 {m['frame']+1}, ID {m['color_id']}: 有TI标注但缺少RGB标注")
                if len(missing_rgb) > 30:
                    report.append(f"  ...以及其他 {len(missing_rgb)-30} 个类似问题")
            
            if size_mismatch:
                report.append("\n▶ 尺寸不匹配:")
                for m in size_mismatch[:30]:
                    rgb_w, rgb_h = m["rgb_size"]
                    ti_w, ti_h = m["ti_size"]
                    report.append(f"  帧 {m['frame']+1}, ID {m['color_id']}: RGB尺寸({int(rgb_w)}×{int(rgb_h)})与TI尺寸({int(ti_w)}×{int(ti_h)})不一致")
                if len(size_mismatch) > 30:
                    report.append(f"  ...以及其他 {len(size_mismatch)-30} 个类似问题")
            
            # 显示报告对话框
            dialog = QDialog(self)
            dialog.setWindowTitle("全局标注一致性检查")
            layout = QVBoxLayout(dialog)
            
            text_edit = QTextEdit()
            text_edit.setReadOnly(True)
            text_edit.setPlainText("\n".join(report))
            layout.addWidget(text_edit)
            
            # 添加操作按钮
            btn_layout = QHBoxLayout()
            
            # 移除"修复尺寸不匹配"按钮
            # 只保留跳转和关闭按钮
            
            # 跳转到第一个问题帧按钮
            if problem_frames:
                jump_btn = QPushButton("跳转到第一个问题帧")
                jump_btn.clicked.connect(lambda: self.jump_to_problem_frame(min(problem_frames), dialog))
                btn_layout.addWidget(jump_btn)
            
            # 关闭按钮
            close_btn = QPushButton("关闭")
            close_btn.clicked.connect(dialog.accept)
            btn_layout.addWidget(close_btn)
            
            layout.addLayout(btn_layout)
            
            dialog.resize(700, 500)
            dialog.exec_()
            
            return False
        
        except Exception as e:
            progress.close()
            self.log_message(f"检查一致性时出错: {str(e)}")
            QMessageBox.warning(self, "错误", f"检查一致性时出错: {str(e)}")
            return False

    def fix_all_annotation_size_mismatches(self, mismatches, parent_dialog=None):
        """修复所有帧的RGB和TI标注的尺寸不匹配"""
        if not mismatches:
            return
        
        # 显示进度对话框
        progress = QDialog(self)
        progress.setWindowTitle("正在修复...")
        progress_layout = QVBoxLayout(progress)
        progress_label = QLabel("正在修复所有尺寸不匹配...")
        progress_layout.addWidget(progress_label)
        progress_bar = QProgressBar()
        progress_bar.setRange(0, len(mismatches))
        progress_bar.setValue(0)
        progress_layout.addWidget(progress_bar)
        progress.resize(300, 100)
        progress.show()
        QApplication.processEvents()
        
        try:
            # 记录已修改的标注数量
            fixed_count = 0
            processed_count = 0
            
            # 记录需要更新的帧
            frames_to_update = set()
            
            for mismatch in mismatches:
                processed_count += 1
                progress_bar.setValue(processed_count)
                progress_label.setText(f"正在修复 {processed_count}/{len(mismatches)}...")
                QApplication.processEvents()
                
                frame = mismatch["frame"]
                color_id = mismatch["color_id"]
                
                # 查找RGB和TI标注
                rgb_ann = None
                ti_ann = None
                
                for ann in self.annotations:
                    if ann.get("frame") == frame and ann.get("color_id") == color_id:
                        if ann.get("modality") == "RGB":
                            rgb_ann = ann
                        elif ann.get("modality") == "TI":
                            ti_ann = ann
                
                if rgb_ann and ti_ann:
                    # 使用RGB标注尺寸作为基准
                    rgb_bbox = rgb_ann.get("bbox", [0, 0, 0, 0])
                    ti_bbox = ti_ann.get("bbox", [0, 0, 0, 0])
                    
                    # 保留TI标注的位置，只同步尺寸 (从RGB到TI的比例)
                    ti_ann["bbox"] = [ti_bbox[0], ti_bbox[1], rgb_bbox[2], rgb_bbox[3]]
                    fixed_count += 1
                    
                    # 记录需要更新的帧
                    frames_to_update.add(frame)
            
            # 关闭进度对话框
            progress.close()
            
            # 更新缓存
            if self.cached_video_id:
                for frame in frames_to_update:
                    frame_annotations = [ann for ann in self.annotations if ann.get("frame") == frame]
                    self.annotation_cache[frame] = frame_annotations.copy()
            
            # 保存修改到磁盘
            # 这里不调用save_annotations_to_disk，因为它只会保存当前帧
            # 改为直接更新所有已修改帧的标注文件
            self.save_all_modified_frames(frames_to_update)
            
            # 显示结果
            msg = f"已修复 {fixed_count} 个标注尺寸不匹配"
            self.log_message(msg)
            QMessageBox.information(self, "修复完成", msg)
            
            # 如果有父对话框，关闭它
            if parent_dialog:
                parent_dialog.accept()
                
        except Exception as e:
            progress.close()
            self.log_message(f"修复尺寸不匹配时出错: {str(e)}")
            QMessageBox.warning(self, "错误", f"修复尺寸不匹配时出错: {str(e)}")

    def save_all_modified_frames(self, frames):
        """保存所有修改过的帧的标注"""
        if not frames:
            return
        
        # 显示进度对话框
        progress = QDialog(self)
        progress.setWindowTitle("正在保存...")
        progress_layout = QVBoxLayout(progress)
        progress_label = QLabel("正在保存修改的标注...")
        progress_layout.addWidget(progress_label)
        progress_bar = QProgressBar()
        progress_bar.setRange(0, len(frames))
        progress_bar.setValue(0)
        progress_layout.addWidget(progress_bar)
        progress.resize(300, 100)
        progress.show()
        QApplication.processEvents()
        
        try:
            # 找出每个帧对应的RGB和TI路径
            frame_paths = {}
            for frame in frames:
                if hasattr(self, 'rgb_frames') and len(self.rgb_frames) > frame:
                    rgb_path = self.rgb_frames[frame]
                    if rgb_path and os.path.exists(rgb_path):
                        rgb_dir = os.path.dirname(rgb_path)
                        if frame not in frame_paths:
                            frame_paths[frame] = {"rgb_dir": None, "ti_dir": None}
                        frame_paths[frame]["rgb_dir"] = rgb_dir
                
                if hasattr(self, 'ti_frames') and len(self.ti_frames) > frame:
                    ti_path = self.ti_frames[frame]
                    if ti_path and os.path.exists(ti_path):
                        ti_dir = os.path.dirname(ti_path)
                        if frame not in frame_paths:
                            frame_paths[frame] = {"rgb_dir": None, "ti_dir": None}
                        frame_paths[frame]["ti_dir"] = ti_dir
            
            # 处理每一帧
            processed_count = 0
            for frame, paths in frame_paths.items():
                processed_count += 1
                progress_bar.setValue(processed_count)
                progress_label.setText(f"正在保存帧 {frame+1}/{max(frames)+1}...")
                QApplication.processEvents()
                
                rgb_dir = paths.get("rgb_dir")
                ti_dir = paths.get("ti_dir")
                
                # 筛选此帧的RGB和TI标注
                frame_rgb_annotations = [ann for ann in self.annotations 
                                        if ann.get("frame") == frame and ann.get("modality") == "RGB"]
                frame_ti_annotations = [ann for ann in self.annotations 
                                        if ann.get("frame") == frame and ann.get("modality") == "TI"]
                
                # 保存RGB标注
                if rgb_dir and frame_rgb_annotations:
                    rgb_annotation_file = os.path.join(rgb_dir, "annotations.json")
                    
                    # 加载现有RGB标注
                    existing_annotations = []
                    if os.path.exists(rgb_annotation_file):
                        try:
                            with open(rgb_annotation_file, 'r', encoding='utf-8') as f:
                                data = json.load(f)
                                # 只加载不是当前帧的标注
                                if "annotations" in data:
                                    existing_annotations = [ann for ann in data["annotations"] 
                                                        if ann.get("frame") != frame]
                        except Exception as e:
                            self.log_message(f"读取RGB标注文件出错: {str(e)}")
                    
                    # 合并现有标注和新标注
                    all_rgb_annotations = existing_annotations + frame_rgb_annotations
                    
                    # 保存RGB标注
                    with open(rgb_annotation_file, 'w', encoding='utf-8') as f:
                        json.dump({"annotations": all_rgb_annotations}, f, ensure_ascii=False, indent=2)
                
                # 保存TI标注
                if ti_dir and frame_ti_annotations:
                    ti_annotation_file = os.path.join(ti_dir, "annotations.json")
                    
                    # 加载现有TI标注
                    existing_annotations = []
                    if os.path.exists(ti_annotation_file):
                        try:
                            with open(ti_annotation_file, 'r', encoding='utf-8') as f:
                                data = json.load(f)
                                # 只加载不是当前帧的标注
                                if "annotations" in data:
                                    existing_annotations = [ann for ann in data["annotations"] 
                                                        if ann.get("frame") != frame]
                        except Exception as e:
                            self.log_message(f"读取TI标注文件出错: {str(e)}")
                    
                    # 合并现有标注和新标注
                    all_ti_annotations = existing_annotations + frame_ti_annotations
                    
                    # 保存TI标注
                    with open(ti_annotation_file, 'w', encoding='utf-8') as f:
                        json.dump({"annotations": all_ti_annotations}, f, ensure_ascii=False, indent=2)
            
            progress.close()
            self.log_message(f"已保存 {len(frames)} 个修改的帧")
            
        except Exception as e:
            progress.close()
            self.log_message(f"保存修改帧时出错: {str(e)}")
            QMessageBox.warning(self, "错误", f"保存修改帧时出错: {str(e)}")

    def jump_to_problem_frame(self, frame, parent_dialog=None):
        """跳转到问题帧"""
        # 如有必要，保存当前帧
        if self.annotation_modified:
            self.save_annotations_to_disk()
        
        # 如果有父对话框，关闭它
        if parent_dialog:
            parent_dialog.accept()
        
        # 更新当前帧索引
        self.current_frame_index = frame
        
        # 更新滑块位置
        self.frame_slider.setValue(self.current_frame_index)
        
        # 加载此帧
        if hasattr(self, "rgb_frames") and frame < len(self.rgb_frames):
            rgb_path = self.rgb_frames[frame]
        else:
            rgb_path = None
            
        if hasattr(self, "ti_frames") and frame < len(self.ti_frames):
            ti_path = self.ti_frames[frame]
        else:
            ti_path = None
        
        # 加载图像
        if rgb_path or ti_path:
            self.load_images(rgb_path, ti_path)
            self.log_message(f"已跳转到问题帧 {frame+1}")
        else:
            self.log_message(f"无法跳转到帧 {frame+1}，图像不存在")

    def toggle_shortcuts(self, enabled):
        """切换快捷键模式"""
        if enabled:
            self.enable_shortcuts()
            self.shortcut_status.setText("已启用")
            self.shortcut_status.setStyleSheet("color: green;")
            self.log_message("快捷键模式已启用")
        else:
            self.disable_shortcuts()
            self.shortcut_status.setText("已禁用")
            self.shortcut_status.setStyleSheet("color: gray;")
            self.log_message("快捷键模式已禁用")
    
    def toggle_play(self):
        """Toggle between playing and pausing frame playback"""
        # 首先确保timer被正确初始化 (修复)
        if not hasattr(self, 'timer') or self.timer is None:
            self.timer = QTimer(self)
            self.timer.timeout.connect(self.play_next_frame)
            
        if not hasattr(self, 'playing'):
            self.playing = False
        
        # Toggle playing state
        self.playing = not self.playing
        
        if (self.playing):
            # Start playback with selected speed
            self.play_button.setText("暂停")
            
            # Get the frame interval based on speed
            interval = self.get_frame_interval()
            self.timer.start(interval)
            self.log_message(f"播放开始 (速度: {self.speed_selector.currentText()})")
        else:
            # Stop playback
            self.play_button.setText("播放")
            if self.timer and self.timer.isActive():
                self.timer.stop()
            self.log_message("播放暂停")
            
            # Fix for color selection issue: restore annotation state after playback
            self.restore_annotation_state()

    def play_next_frame(self):
        """Play the next frame during automatic playback"""
        total_frames = 0
        
        if hasattr(self, "rgb_frames") and self.rgb_frames:
            total_frames = len(self.rgb_frames)
        elif hasattr(self, "ti_frames") and self.ti_frames:
            total_frames = len(self.ti_frames)
        
        if total_frames == 0:
            self.toggle_play()  # Stop playback if no frames
            return
            
        # Check if we're at the end of the video
        if (self.current_frame_index >= total_frames - 1):
            self.toggle_play()  # Stop playback and restore annotation state
            self.log_message("已到达最后一帧")
            return
            
        # Advance to next frame
        self.next_frame()
        
    def restore_annotation_state(self):
        """恢复标注功能，确保标注能力可用"""
        # 获取当前选中的颜色
        selected = self.annotation_button_group.checkedId()
        if selected != -1:
            btn = self.annotation_button_group.checkedButton()
            color_text = btn.styleSheet()
            if "color:" in color_text:
                color = color_text.split("color:")[1].split(";")[0].strip()
            else:
                color = self.color_map.get(selected, "#FF0000")
                
            # 设置标注启用状态
            if self.current_single_mode == "RGB":
                self.rgb_view.set_annotation_enabled(True)
                self.rgb_view.set_current_color(selected, color)
                self.ti_view.set_annotation_enabled(False)
            else:
                self.ti_view.set_annotation_enabled(True)
                self.ti_view.set_current_color(selected, color)
                self.rgb_view.set_annotation_enabled(False)
                
            self.log_message(f"已恢复标注状态，当前选择: ID{selected}")
        else:
            self.rgb_view.set_annotation_enabled(False)
            self.ti_view.set_annotation_enabled(False)

    def update_annotation_storage(self, new_ann: dict):
        """更新标注存储：同一帧、同颜色、同模态仅保留最新标注"""
        # Only remove annotations that have the same frame, color_id AND modality
        self.annotations = [ann for ann in self.annotations 
                            if not (ann["frame"] == new_ann["frame"] and 
                                    ann["color_id"] == new_ann["color_id"] and 
                                    ann["modality"] == new_ann["modality"])]
        
        # Add the new annotation
        self.annotations.append(new_ann)
        
        # Update bbox tree display
        self.update_bbox_tree()
        
        # Update the cache
        if self.cached_video_id:
            if self.current_frame_index not in self.annotation_cache:
                self.annotation_cache[self.current_frame_index] = []
            
            # Update cache by removing matching annotation and adding new one
            self.annotation_cache[self.current_frame_index] = [
                ann for ann in self.annotation_cache[self.current_frame_index]
                if not (ann["color_id"] == new_ann["color_id"] and ann["modality"] == new_ann["modality"])
            ]
            self.annotation_cache[self.current_frame_index].append(new_ann.copy())

    def update_bbox_tree(self):
        """更新标注信息树显示：只显示当前帧的标注"""
        self.bbox_tree.clear()
        
        # Filter to only show annotations for current frame
        current_frame_annotations = [ann for ann in self.annotations if ann.get("frame") == self.current_frame_index]
        
        for ann in current_frame_annotations:
            color_id = ann["color_id"]
            x, y, width, height = ann["bbox"]
            modality = ann["modality"]
            
            # 如果是RGB标注，并且当前是原始分辨率模式，需要转换坐标以正确显示
            display_x, display_y, display_width, display_height = [x, y, width, height]
            if modality == "RGB" and self.show_full_resolution:
                display_x, display_y, display_width, display_height = self.convert_annotation_coordinates(
                    [x, y, width, height], to_original=True)
            
            item = QTreeWidgetItem([
                f"{color_id}",
                f"x:{int(display_x)}, y:{int(display_y)}",
                f"w:{int(display_width)}, h:{int(display_height)}",
                modality
            ])
            item.setData(0, Qt.UserRole, ann)
            color = self.color_map.get(color_id, "#888888")
            for i in range(4):
                item.setBackground(i, QColor(color).lighter(150))
            self.bbox_tree.addTopLevelItem(item)

    def on_annotation_selected_in_tree(self):
        """处理在标注树中选择项目，更新编辑控件"""
        selected_items = self.bbox_tree.selectedItems()
        if not selected_items:
            # 如果没有选择，禁用编辑控件
            self.disable_edit_controls()
            
            # 如果没有选择，再次启用所有标注功能
            self.enable_all_annotations()
            return
            
        item = selected_items[0]
        ann = item.data(0, Qt.UserRole)
        if not ann:
            self.disable_edit_controls()
            return
            
        color_id = ann["color_id"]
        
        # 获取原始标注坐标
        x, y, width, height = ann["bbox"]
        modality = ann["modality"]
        
        # 如果是RGB标注，并且当前是原始分辨率模式，需要转换坐标以正确高亮
        if modality == "RGB" and self.show_full_resolution:
            x, y, width, height = self.convert_annotation_coordinates(
                [x, y, width, height], to_original=True)
        
        rect = QRectF(x, y, width, height)
        
        # 禁用这个颜色ID的标注功能
        self.disable_annotation_for_color(color_id)
        
        # 高亮显示选中的标注
        self.highlight_annotation(color_id, rect, modality)
        
        # 更新编辑控件中的值
        self.edit_x.blockSignals(True)
        self.edit_y.blockSignals(True)
        self.edit_w.blockSignals(True)
        self.edit_h.blockSignals(True)
        
        self.edit_x.setValue(int(x))
        self.edit_y.setValue(int(y))
        self.edit_w.setValue(int(width))
        self.edit_h.setValue(int(height))
        
        self.edit_x.blockSignals(False)
        self.edit_y.blockSignals(False)
        self.edit_w.blockSignals(False)
        self.edit_h.blockSignals(False)
        
        # 启用编辑控件
        self.enable_edit_controls()
        
        # 使用正确的显示坐标记录日志
        if modality == "RGB" and self.show_full_resolution:
            self.log_message(f"{modality}标注 ID{color_id}: ({int(x)},{int(y)},{int(width)}×{int(height)})")
        else:
            self.log_message(f"{modality}标注 ID{color_id}: ({int(rect.x())},{int(rect.y())},{int(rect.width())}×{int(rect.height())})")

    def highlight_annotation(self, color_id, rect, modality):
        """高亮显示选中的标注框"""
        try:
            # 清除之前的高亮
            if self.highlighted_item and not sip.isdeleted(self.highlighted_item):
                try:
                    scene = self.highlighted_item.scene()
                    if scene:
                        scene.removeItem(self.highlighted_item)
                except (RuntimeError, TypeError, Exception) as e:
                    logger.error(f"清除高亮标注出错: {e}")
                    pass
                
            self.highlighted_item = None
            
            # 选择正确的视图
            view = self.rgb_view if modality == "RGB" else self.ti_view
            
            # 确保视图有效
            if not view or not view.scene():
                self.log_message("无法高亮标注：视图或场景无效")
                return
            
            # 使用特殊的、可区分的虚线风格绘制高亮框
            pen = QPen(QColor(self.color_map.get(color_id, "#FFFFFF")))
            pen.setWidth(4)
            pen.setStyle(Qt.DashLine)
            
            # 在当前框正确位置创建一个新的高亮框
            rect_item = view.scene().addRect(QRectF(rect), pen)
                
            # 设置Z值确保高亮框在顶层
            rect_item.setZValue(100)
                
            # 保存高亮框引用
            rect_item.annotation_data = {'color_id': color_id, 'modality': modality, 'is_highlight': True}
            self.highlighted_item = rect_item
                
            # 强制更新视图确保变化立即可见
            view.viewport().update()
                
        except Exception as e:
            self.log_message(f"创建高亮框时错误: {str(e)}")

    def redraw_all_annotations(self):
        """重新绘制当前帧的所有标注"""
        
        # 清除视图标注（保留高亮框）
        self.rgb_view.clearAnnotations(keep_highlight=True)
        self.ti_view.clearAnnotations(keep_highlight=True)
        
        if not self.annotations:
            return
            
        # Clear any previous annotations first
        self.rgb_view.clearAnnotations()
        self.ti_view.clearAnnotations()
        
        # Count how many annotations are drawn for each modality
        rgb_count = 0
        ti_count = 0
        
        for ann in self.annotations:
            if ann.get("frame") != self.current_frame_index:
                continue
                
            try:
                color_id = ann.get("color_id")
                if not color_id:
                    continue
                    
                # Get rectangle coordinates
                if isinstance(ann.get("bbox"), list) and len(ann.get("bbox")) == 4:
                    x, y, width, height = ann["bbox"]
                    
                    # 如果是RGB标注，并且当前是原始分辨率模式，需要转换坐标
                    if ann.get("modality") == "RGB" and self.show_full_resolution:
                        # 标注已经以缩放后的坐标储存，需要转为原始坐标
                        x, y, width, height = self.convert_annotation_coordinates(
                            [x, y, width, height], to_original=True)
                    
                    rect = QRectF(x, y, width, height)
                else:
                    logger.warning(f"异常的标注框格式: {ann.get('bbox')}")
                    continue
                    
                # Get color from colormap or default to white
                color = self.color_map.get(color_id, "#FFFFFF")
                
                # Get modality (default to RGB if not specified)
                modality = ann.get("modality", "RGB")
                
                # Draw on the appropriate view
                view = self.rgb_view if modality == "RGB" else self.ti_view
                rect_item = view.drawAnnotation(rect, color, color_id)
                
                # Count annotations by modality
                if modality == "RGB":
                    rgb_count += 1
                else:
                    ti_count += 1
                    
            except Exception as e:
                logger.error(f"渲染标注时出错: {e}, 数据: {ann}")
        
        # Print summary of what we loaded
        if rgb_count > 0 or ti_count > 0:
            self.log_message(f"已渲染 RGB标注:{rgb_count}个, TI标注:{ti_count}个")
            
        # Force UI update to ensure annotations are immediately visible
        self.rgb_view.viewport().update()
        self.ti_view.viewport().update()

    def delete_selected_annotation(self):
        """删除选中的标注"""
        selected_items = self.bbox_tree.selectedItems()
        if not selected_items:
            self.log_message("未选择任何标注")
            return
        item = selected_items[0]
        ann = item.data(0, Qt.UserRole)
        if not ann:
            return
        
        # 从内部数据结构中删除此标注
        self.annotations = [a for a in self.annotations if not (
            a["frame"] == ann["frame"] and 
            a["color_id"] == ann["color_id"] and 
            a["modality"] == ann["modality"]
        )]
        
        # 从annotation_manager中也删除此标注（如果可用）
        if self.annotation_manager and self.current_project:
            color_id = ann["color_id"]
            modality = ann["modality"]
            # 找到对应的workspace标注并删除
            workspace_anns = self.annotation_manager.get_workspace_annotations(self.current_project)
            for ws_ann in workspace_anns:
                if (ws_ann.get("frame") == ann["frame"] and 
                    ws_ann.get("color_id") == ann["color_id"] and 
                    ws_ann.get("modality") == ann["modality"]):
                    # 如果有_id，使用_id删除；否则直接修改workspace列表
                    if "_id" in ws_ann:
                        self.annotation_manager.remove_workspace_annotation(
                            self.current_project, ws_ann["_id"])
                        break
        
        # 清除高亮框
        if self.highlighted_item and not sip.isdeleted(self.highlighted_item):
            try:
                scene = self.highlighted_item.scene()
                if scene:
                    scene.removeItem(self.highlighted_item)
            except RuntimeError:
                pass
        self.highlighted_item = None
        
        # 清除两个视图中的所有标注，然后重新绘制其他标注
        self.rgb_view.clearAnnotations()
        self.ti_view.clearAnnotations()
        
        # 更新bbox_tree视图
        self.update_bbox_tree()
        
        # 重新绘制剩余标注
        self.redraw_all_annotations()
        
        # 保存更改到annotations.json文件 (新增)
        self.save_annotations_to_disk()
        
        # 显示反馈消息
        self.log_message(f"已删除标注: ID {ann['color_id']}")
        
        # Mark that annotations have been modified    
        self.annotation_modified = True
        
        # Update the cache with the new annotations
        if self.cached_video_id and self.current_frame_index is not None:
            frame_annotations = [ann for ann in self.annotations if ann.get("frame") == self.current_frame_index]
            self.annotation_cache[self.current_frame_index] = frame_annotations.copy()

    def load_current_frame(self, force_reload=False):
        """Load the current frame based on display mode and clear previous annotations"""
        # First clear any existing highlights
        if self.highlighted_item and not sip.isdeleted(self.highlighted_item):
            try:
                scene = self.highlighted_item.scene()
                if scene:
                    scene.removeItem(self.highlighted_item)
            except RuntimeError:
                pass
        self.highlighted_item = None
        
        # Clear any tree selection
        if hasattr(self, 'bbox_tree'):
            self.bbox_tree.clearSelection()
        
        # IMPORTANT: Clear all annotations before loading new frame
        self.annotations = []
        self.rgb_view.clearAnnotations()
        self.ti_view.clearAnnotations()
        
        # Load the appropriate images based on current display mode
        if self.two_column_mode:
            self.load_both_modalities(force_reload)
        else:
            if self.current_single_mode == "RGB":
                self.load_rgb_frame(force_reload)
            else:
                self.load_ti_frame(force_reload)
        
        # Load annotations from JSON file corresponding to current frame
        self.load_frame_annotations()
        
        # Update bbox tree based on the newly loaded annotations
        self.update_bbox_tree()

    def load_frame_annotations(self):
        """Load annotations from cached data or JSON file for current frame"""
        if not hasattr(self, 'rgb_frames') and not hasattr(self, 'ti_frames'):
            return
        
        # Only clear visual annotations, not the data structure
        self.rgb_view.clearAnnotations()
        self.ti_view.clearAnnotations()
        
        # First try to load from cache for quick access
        current_frame_annotations = []
        
        if self.cached_video_id and self.current_frame_index in self.annotation_cache:
            cached_annotations = self.annotation_cache[self.current_frame_index]
            if cached_annotations:
                self.log_message(f"从缓存加载标注: {len(cached_annotations)} 项")
                
                # Get current frame annotations
                current_frame_annotations = cached_annotations.copy()
                
                # Filter out annotations for the current frame from the existing list
                self.annotations = [ann for ann in self.annotations 
                                if ann.get("frame") != self.current_frame_index]
                
                # Add cached annotations to the main list
                for ann in current_frame_annotations:
                    self.annotations.append(ann.copy())  # Use a copy to avoid modifying cache
        
        # If not in cache or cache empty, then try loading from file
        else:
            self.is_loading_annotations = True
            self.log_message("正在加载标注...")
            
            # Try to find annotations.json file in the frame directory
            annotation_file = None
            frame_dir = None
            
            # 重要: 优先根据当前模态查找标注文件
            if self.current_single_mode == "TI" and hasattr(self, 'ti_frames') and self.ti_frames and self.current_frame_index < len(self.ti_frames):
                ti_path = self.ti_frames[self.current_frame_index]
                if ti_path and os.path.exists(ti_path):
                    frame_dir = os.path.dirname(ti_path)
                    annotation_file = os.path.join(frame_dir, "annotations.json")
                    self.log_message(f"尝试加载TI标注: {annotation_file}")
            elif hasattr(self, 'rgb_frames') and self.rgb_frames and self.current_frame_index < len(self.rgb_frames):
                rgb_path = self.rgb_frames[self.current_frame_index]
                if rgb_path and os.path.exists(rgb_path):
                    frame_dir = os.path.dirname(rgb_path)
                    annotation_file = os.path.join(frame_dir, "annotations.json")
                    self.log_message(f"尝试加载RGB标注: {annotation_file}")
            
            # 如果没找到，尝试查找另一个模态的标注
            if (not annotation_file or not os.path.exists(annotation_file)):
                if self.current_single_mode == "RGB" and hasattr(self, 'ti_frames') and self.ti_frames and self.current_frame_index < len(self.ti_frames):
                    ti_path = self.ti_frames[self.current_frame_index]
                    if ti_path and os.path.exists(ti_path):
                        frame_dir = os.path.dirname(ti_path)
                        annotation_file = os.path.join(frame_dir, "annotations.json")
                        self.log_message(f"尝试加载TI标注: {annotation_file}")
                elif hasattr(self, 'rgb_frames') and self.rgb_frames and self.current_frame_index < len(self.rgb_frames):
                    rgb_path = self.rgb_frames[self.current_frame_index]
                    if rgb_path and os.path.exists(rgb_path):
                        frame_dir = os.path.dirname(rgb_path)
                        annotation_file = os.path.join(frame_dir, "annotations.json")
                        self.log_message(f"尝试加载RGB标注: {annotation_file}")
            
            # Check if we should look in parent directory (shared annotations)
            if not annotation_file or not os.path.exists(annotation_file):
                if frame_dir:
                    parent_dir = os.path.dirname(frame_dir)
                    parent_annotation_file = os.path.join(parent_dir, "annotations.json")
                    if os.path.exists(parent_annotation_file):
                        annotation_file = parent_annotation_file
                        self.log_message(f"尝试加载共享标注: {annotation_file}")
            
            # If annotations file found, load it
            if annotation_file and os.path.exists(annotation_file):
                try:
                    with open(annotation_file, 'r', encoding='utf-8') as f:
                        data = json.load(f)
                        
                    # Load annotations relevant to the current frame
                    if isinstance(data, dict) and "annotations" in data:
                        # First, remove any existing annotations for this frame
                        self.annotations = [ann for ann in self.annotations 
                                        if ann.get("frame") != self.current_frame_index]
                        
                        # Get only annotations for the current frame
                        current_frame_annotations = [ann for ann in data.get("annotations", []) 
                                                if ann.get("frame") == self.current_frame_index]
                        
                        # Add file annotations to the main list
                        for ann in current_frame_annotations:
                            self.annotations.append(ann)
                        
                        # Update cache for future quick access
                        if self.cached_video_id:
                            self.annotation_cache[self.current_frame_index] = current_frame_annotations.copy()
                        
                        self.log_message(f"已加载标注: {len(current_frame_annotations)} 项")
                    else:
                        self.log_message("标注文件格式不正确")
                            
                except Exception as e:
                    logger.error(f"加载标注文件出错: {e}")
                    self.log_message(f"加载标注失败: {str(e)}")
            else:
                self.log_message("没有找到标注文件")
                
                # If no file found, make sure we don't have stale annotations for this frame
                self.annotations = [ann for ann in self.annotations 
                                if ann.get("frame") != self.current_frame_index]

            self.is_loading_annotations = False

        # 延迟重绘所有标注，确保TI模态也能正确显示
        QTimer.singleShot(50, self.redraw_all_annotations)
        
        # Update annotation tree
        self.update_bbox_tree()
        self.annotation_modified = False  # Reset modification flag
        
    def redraw_all_annotations(self):
        """重新绘制当前帧的所有标注"""
        if not self.annotations:
            return
                
        # Clear any previous annotations first
        self.rgb_view.clearAnnotations()
        self.ti_view.clearAnnotations()
        
        # Count how many annotations are drawn for each modality
        rgb_count = 0
        ti_count = 0
        
        for ann in self.annotations:
            if ann.get("frame") != self.current_frame_index:
                continue
                    
            try:
                color_id = ann.get("color_id")
                if not color_id:
                    continue
                        
                # Get rectangle coordinates
                if isinstance(ann.get("bbox"), list) and len(ann.get("bbox")) == 4:
                    x, y, width, height = ann["bbox"]
                    
                    # 获取模态信息
                    modality = ann.get("modality", "RGB")
                    
                    # 如果是RGB标注，并且当前是原始分辨率模式，需要转换坐标
                    if modality == "RGB" and self.show_full_resolution:
                        # 标注已经以缩放后的坐标储存，需要转为原始坐标
                        orig_coords = self.convert_annotation_coordinates(
                            [x, y, width, height], to_original=True)
                        rect = QRectF(orig_coords[0], orig_coords[1], orig_coords[2], orig_coords[3])
                    else:
                        rect = QRectF(x, y, width, height)
                else:
                    logger.warning(f"异常的标注框格式: {ann.get('bbox')}")
                    continue
                        
                # Get color from colormap or default to white
                color = self.color_map.get(color_id, "#FFFFFF")
                
                # Draw on the appropriate view
                view = self.rgb_view if modality == "RGB" else self.ti_view
                rect_item = view.drawAnnotation(rect, color, color_id, modality=modality)
                
                # Count annotations by modality
                if modality == "RGB":
                    rgb_count += 1
                else:
                    ti_count += 1
                        
            except Exception as e:
                logger.error(f"渲染标注时出错: {e}, 数据: {ann}")
        
        # Print summary of what we loaded
        if rgb_count > 0 or ti_count > 0:
            self.log_message(f"已渲染 RGB标注:{rgb_count}个, TI标注:{ti_count}个")
                
        # Force UI update to ensure annotations are immediately visible
        self.rgb_view.viewport().update()
        self.ti_view.viewport().update()

    def on_offset_changed(self, val):
        """处理偏移滑块变化 - 现用于标注同步偏移而非画面移动"""
        x = self.offset_slider_x.value()
        y = self.offset_slider_y.value()
        self.offsetChanged.emit(x, y)
        
        # 不再调用update_movement()移动画面，而是更新偏移状态
        self.log_message(f"标注同步偏移已设置：X={x}, Y={y}")
        
        # 如果当前选中了标注且同时标注模式开启，立即应用偏移更新两个模态的标注
        if self.simultaneous_annotation and hasattr(self, 'selected_annotation') and self.selected_annotation:
            self.apply_offset_to_selected_annotation()
        elif self.simultaneous_annotation:
            # 如果同时标注模式开启但没有选中标注，只更新用于后续标注的偏移值
            self.log_message("偏移设置已保存，将用于后续同时标注")

    def apply_offset_to_selected_annotation(self):
        """应用当前偏移到选中的标注上，更新对应的同步标注"""
        # 获取当前选中的标注
        selected_items = self.bbox_tree.selectedItems()
        if not selected_items:
            return
        
        item = selected_items[0]
        ann = item.data(0, Qt.UserRole)
        if not ann:
            return
        
        # 获取选中标注的基本信息
        frame = ann.get("frame")
        color_id = ann.get("color_id")
        modality = ann.get("modality")
        bbox = ann.get("bbox")
        
        # 查找对应模态的标注
        target_modality = "TI" if modality == "RGB" else "RGB"
        target_ann = None
        
        for a in self.annotations:
            if (a.get("frame") == frame and 
                a.get("color_id") == color_id and 
                a.get("modality") == target_modality):
                target_ann = a
                break
        
        # 如果找不到对应的标注，无法应用偏移
        if not target_ann:
            self.log_message(f"无法找到对应的{target_modality}标注来应用偏移")
            return
        
        # 获取当前偏移值
        offset_x = self.offset_slider_x.value()
        offset_y = self.offset_slider_y.value()
        
        # 应用偏移值更新对应标注位置
        source_rect = QRectF(*bbox)
        
        # 使用修改后的transform_coordinates_between_modalities方法转换坐标
        target_rect = self.transform_coordinates_between_modalities(
            source_rect, modality, target_modality)
        
        # 更新目标标注的bbox
        target_ann["bbox"] = [target_rect.x(), target_rect.y(), 
                            target_rect.width(), target_rect.height()]
        
        # 更新显示
        self.redraw_all_annotations()
        self.update_bbox_tree()
        
        # 标记为已修改
        self.annotation_modified = True
        
        # 更新缓存
        if self.cached_video_id:
            frame_annotations = [ann for ann in self.annotations if ann.get("frame") == self.current_frame_index]
            self.annotation_cache[self.current_frame_index] = frame_annotations.copy()
        
        self.log_message(f"已应用{modality}到{target_modality}的标注偏移 (X:{offset_x}, Y:{offset_y})")

    def apply_sync_offset_to_selected(self):
        """应用当前偏移到选中的标注上"""
        if not hasattr(self, 'selected_annotation') or not self.selected_annotation:
            return
            
        # 获取当前偏移值
        offset_x = self.offset_slider_x.value()
        offset_y = self.offset_slider_y.value()
        
        # 应用偏移值更新标注位置
        self.adjust_annotation_position(offset_x, offset_y, use_step=False)
        
        # 重置偏移滑块以便下次使用
        self.offset_slider_x.blockSignals(True)
        self.offset_slider_y.blockSignals(True)
        self.offset_slider_x.setValue(0)
        self.offset_slider_y.setValue(0)
        self.offset_slider_x.blockSignals(False)
        self.offset_slider_y.blockSignals(False)
        
    def update_movement(self):
        """Apply translation offset to loaded images and update display"""
        if not hasattr(self, 'rgb_resized') and not hasattr(self, 'ti'):
            return
            
        offset_x = self.offset_slider_x.value()
        offset_y = self.offset_slider_y.value()
        M = np.float32([[1, 0, offset_x], [0, 1, offset_y]])
        
        # Process RGB image if available
        if hasattr(self, 'rgb_resized') and self.rgb_resized is not None:
            try:
                moved_rgb = cv2.warpAffine(self.rgb_resized, M, (self.rgb_resized.shape[1], self.rgb_resized.shape[0]))
                pixmap_rgb = cv2_to_qpixmap(moved_rgb)
                self.rgb_view.scene().clear()
                self.rgb_view.scene().addPixmap(pixmap_rgb)
                self.rgb_view.scene().setSceneRect(QRectF(pixmap_rgb.rect()))
            except Exception as e:
                logger.error(f"Error applying RGB movement: {e}")
        
        # Process TI image if available
        if hasattr(self, 'ti') and self.ti is not None:
            try:
                moved_ti = cv2.warpAffine(self.ti, M, (self.ti.shape[1], self.ti.shape[0]))
                pixmap_ti = cv2_to_qpixmap(moved_ti)
                self.ti_view.scene().clear()
                self.ti_view.scene().addPixmap(pixmap_ti)
                self.ti_view.scene().setSceneRect(QRectF(pixmap_ti.rect()))
            except Exception as e:
                logger.error(f"Error applying TI movement: {e}")
        else:
            self.ti_view.scene().clear()
            self.ti_filename_edit.setText("N/A")
            self.ti_filepath_edit.setText("N/A")
    
    def _auto_fit_views(self):
        """Auto-fit images to views based on current display mode"""
        if not self.autoFit:
            return  # 如果不是自动适应模式，不做任何处理
            
        if self.two_column_mode:
            # In dual column mode, fit both views
            if self.rgb_view.scene() and self.rgb_view.scene().sceneRect().isValid():
                self.rgb_view.fitInView(self.rgb_view.scene().sceneRect(), Qt.KeepAspectRatio)
            if self.ti_view.scene() and self.ti_view.scene().sceneRect().isValid():
                self.ti_view.fitInView(self.ti_view.scene().sceneRect(), Qt.KeepAspectRatio)
        else:
            # In single column mode, fit only the visible view
            current_view = self.rgb_view if self.current_single_mode == "RGB" else self.ti_view
            if current_view.scene() and current_view.scene().sceneRect().isValid():
                current_view.fitInView(current_view.scene().sceneRect(), Qt.KeepAspectRatio)

    def show_metadata(self, metadata):
        """Display metadata summary in the UI"""
        if not metadata:
            return
            
        info = [
            f"时间: {metadata.get('timestamp', '')}",
            f"GPS: {metadata.get('latitude', '')}, {metadata.get('longitude', '')}",
            f"高度: {metadata.get('rel_alt', '')}m",
            f"焦距: {metadata.get('focal_len', '')}mm"
        ]
        
        # Create metadata label if it doesn't exist
        if not hasattr(self, 'meta_label'):
            self.meta_label = QLabel()
            self.control_panel.layout().addWidget(self.meta_label)
        self.meta_label.setText('\n'.join(info))
        
    def syncFromRGB(self, rect, color_id=None, color=None):
        """RGB视图上的标注同步到TI视图（如果设置了同步）"""
        selected = color_id if color_id is not None else self.annotation_button_group.checkedId()
        if selected == -1:
            self.log_message("未选择标注颜色！")
            return
        
        # 获取颜色信息
        if color is not None:
            current_color = color
        else:
            btn = self.annotation_button_group.checkedButton()
            current_color = btn.styleSheet().split("color:")[1].split(";")[0].strip()
        
        # 获取标注坐标
        x, y, width, height = rect.x(), rect.y(), rect.width(), rect.height()
        
        # 在原始分辨率模式下，将原始坐标转换为缩放坐标以保存
        if self.show_full_resolution:
            # 储存转换后的坐标
            scaled_x, scaled_y, scaled_width, scaled_height = self.convert_annotation_coordinates(
                [x, y, width, height], to_original=False)
        else:
            # 如果不是原始分辨率模式，直接使用原始坐标
            scaled_x, scaled_y, scaled_width, scaled_height = x, y, width, height
        
        # 创建标注数据(总是保存缩放后的坐标)
        new_ann = {
            "frame": self.current_frame_index,
            "bbox": [scaled_x, scaled_y, scaled_width, scaled_height],
            "color_id": selected,
            "modality": "RGB"
        }
        
        # 清除RGB视图的旧标注并绘制新标注
        self.rgb_view.clearColorAnnotation(selected, modality="RGB")
        item = self.rgb_view.drawAnnotation(rect, current_color, selected, modality="RGB")
        self.drawn_annotations[selected] = item
        
        # 记录标注信息并保存RGB标注
        self.log_message(f"RGB标注 ID{selected}: ({int(rect.x())},{int(rect.y())},{int(rect.width())}×{int(rect.height())})")
        self.update_annotation_storage(new_ann)
        
        # 处理标注的TI同步（无论是否在双栏模式）
        if self.simultaneous_annotation:
            # 同时标注：将RGB坐标转换到TI坐标系
            ti_rect = self.transform_coordinates_between_modalities(rect, "RGB", "TI")
            
            # 在TI视图创建对应的标注
            self.ti_view.clearColorAnnotation(selected, modality="TI")
            ti_item = self.ti_view.drawAnnotation(ti_rect, current_color, selected, modality="TI")
            
            # 保存TI标注
            ti_ann = {
                "frame": self.current_frame_index,
                "bbox": [ti_rect.x(), ti_rect.y(), ti_rect.width(), ti_rect.height()],
                "color_id": selected,
                "modality": "TI"
            }
            self.update_annotation_storage(ti_ann)
            self.log_message(f"已同步创建TI标注 ID{selected}（已应用X:{self.offset_slider_x.value()},Y:{self.offset_slider_y.value()}偏移）")
        
        # 更新缓存和树视图
        self.update_bbox_tree()
        if self.cached_video_id:
            frame_annotations = [ann for ann in self.annotations if ann.get("frame") == self.current_frame_index]
            self.annotation_cache[self.current_frame_index] = frame_annotations.copy()
        
        # 标记为已修改
        self.annotation_modified = True
    
    def syncFromTI(self, rect, color_id=None, color=None):
        """TI视图上的标注同步到RGB视图（如果设置了同步）"""
        selected = color_id if color_id is not None else self.annotation_button_group.checkedId()
        if selected == -1:
            self.log_message("未选择标注颜色！")
            return
        
        # 获取颜色信息
        if color is not None:
            current_color = color
        else:
            btn = self.annotation_button_group.checkedButton()
            current_color = btn.styleSheet().split("color:")[1].split(";")[0].strip()
        
        # 获取标注坐标
        x, y, width, height = rect.x(), rect.y(), rect.width(), rect.height()
        
        # 清除TI视图的旧标注并绘制新标注
        self.ti_view.clearColorAnnotation(selected, modality="TI")
        item = self.ti_view.drawAnnotation(rect, current_color, selected, modality="TI")
        self.drawn_annotations[selected] = item
        
        # 记录标注信息并保存TI标注
        self.log_message(f"TI标注 ID{selected}: ({int(rect.x())},{int(rect.y())},{int(rect.width())}×{int(rect.height())})")
        new_ann = {
            "frame": self.current_frame_index,
            "bbox": [rect.x(), rect.y(), rect.width(), rect.height()],
            "color_id": selected,
            "modality": "TI"
        }
        self.update_annotation_storage(new_ann)
        
        # 处理标注的RGB同步
        if self.simultaneous_annotation:
            # 同时标注：将TI坐标转换到RGB坐标系
            rgb_rect = self.transform_coordinates_between_modalities(rect, "TI", "RGB")
            
            # 清除并绘制RGB标注
            self.rgb_view.clearColorAnnotation(selected, modality="RGB")
            
            # 在原始分辨率模式下，需要转换坐标
            if self.show_full_resolution:
                # 应用原始分辨率转换
                orig_coords = self.convert_annotation_coordinates(
                    [rgb_rect.x(), rgb_rect.y(), rgb_rect.width(), rgb_rect.height()], 
                    to_original=True)
                display_rgb_rect = QRectF(*orig_coords)
                
                # 为保存准备的缩放坐标
                save_coords = [rgb_rect.x(), rgb_rect.y(), rgb_rect.width(), rgb_rect.height()]
            else:
                display_rgb_rect = rgb_rect
                save_coords = [rgb_rect.x(), rgb_rect.y(), rgb_rect.width(), rgb_rect.height()]
            
            # 绘制新的RGB标注
            rgb_item = self.rgb_view.drawAnnotation(display_rgb_rect, current_color, selected, modality="RGB")
            
            # 保存RGB标注
            rgb_ann = {
                "frame": self.current_frame_index,
                "bbox": save_coords,
                "color_id": selected,
                "modality": "RGB"
            }
            
            self.update_annotation_storage(rgb_ann)
            self.log_message(f"已同步创建RGB标注 ID{selected}（已应用X:{self.offset_slider_x.value()},Y:{self.offset_slider_y.value()}偏移）")
        
        # 更新缓存和树视图
        self.update_bbox_tree()
        if self.cached_video_id:
            frame_annotations = [ann for ann in self.annotations if ann.get("frame") == self.current_frame_index]
            self.annotation_cache[self.current_frame_index] = frame_annotations.copy()
        
        # 标记为已修改
        self.annotation_modified = True

    def update_annotation_info_tree(self):
        """Update the annotation info tree display"""
        self.annotation_info_tree.clear()
        current_frame_annotations = [ann for ann in self.annotations if ann["frame"] == self.current_frame_index]
        for ann in current_frame_annotations:
            color_id = ann["color_id"]
            x, y, width, height = ann["bbox"]
            modality = ann["modality"]
            item = QTreeWidgetItem([
                f"{color_id}",
                f"x:{int(x)}, y:{int(y)}",
                f"w:{int(width)}, h:{int(height)}",
                modality
            ])
            item.setData(0, Qt.UserRole, ann)
            color = self.color_map.get(color_id, "#888888")
            for i in range(4):
                item.setBackground(i, QColor(color).lighter(150))
            self.annotation_info_tree.addTopLevelItem(item)

    def toggleLayout(self):
        """Toggle between single and dual column display modes"""
        if self.two_column_mode:
            self.two_column_mode = False
            self.ti_view.hide()
            if self.current_single_mode == "RGB":
                self.rgb_view.show()
                self.ti_view.hide()
            else:
                self.rgb_view.hide()
                self.ti_view.show()
            self.rgb_view.resetTransform()
            self.toggle_layout_button.setText("切换为双栏显示")
            self.autoFit = False  # 保持原始分辨率模式
            self.toggle_modality_button.show()  # 确保模态切换按钮显示
        else:
            self.two_column_mode = True
            self.rgb_view.show()
            self.ti_view.show()
            self.rgb_view.resetTransform()
            self.ti_view.resetTransform()
            self.toggle_layout_button.setText("切换为单栏显示")
            self.autoFit = False  # 保持原始分辨率模式
            self.toggle_modality_button.hide()
            
            # 添加：在双栏模式下，更新颜色状态
            self.update_annotation_state_for_dual_mode()
            
            self.load_current_frame(force_reload=True)  # 强制重新加载确保原始分辨率模式
    
    def update_annotation_state_for_dual_mode(self):
        """在双栏模式下，更新标注状态，确保颜色设置正确"""
        selected = self.annotation_button_group.checkedId()
        if selected != -1:
            btn = self.annotation_button_group.checkedButton()
            color_text = btn.styleSheet()
            if "color:" in color_text:
                color = color_text.split("color:")[1].split(";")[0].strip()
            else:
                color = self.color_map.get(selected, "#FF0000")
            
            # 双栏模式下，始终启用两个视图的标注功能
            self.rgb_view.set_annotation_enabled(True)
            self.ti_view.set_annotation_enabled(True)
            self.rgb_view.set_current_color(selected, color)
            self.ti_view.set_current_color(selected, color)
            self.log_message(f"双栏模式：已同时启用RGB和TI标注，ID {selected}")
        else:
            self.rgb_view.set_annotation_enabled(False)
            self.ti_view.set_annotation_enabled(False)




    def toggleModality(self):
        """Toggle between RGB and TI views in single column mode"""
        if self.current_single_mode == "RGB":
            self.current_single_mode = "TI"
            self.rgb_view.hide()
            self.ti_view.show()
            self.toggle_modality_button.setText("显示RGB")
            
            # 保存当前标注状态
            any_annotation_modified = self.annotation_modified
            if any_annotation_modified:
                self.save_annotations_to_disk()
            
            # 重置视图变换，解决TI图像过度放大问题
            self.ti_view.resetTransform()
            
            # 获取当前选择的颜色
            selected = self.annotation_button_group.checkedId()
            if selected != -1:
                btn = self.annotation_button_group.checkedButton()
                color_text = btn.styleSheet()
                if "color:" in color_text:
                    color = color_text.split("color:")[1].split(";")[0].strip()
                else:
                    color = self.color_map.get(selected, "#FF0000")
                    
                self.ti_view.set_annotation_enabled(True)
                self.ti_view.set_current_color(selected, color)
                self.rgb_view.set_annotation_enabled(False)
                
                # 同步框大小设置
                if hasattr(self, 'sync_bbox_size') and self.sync_bbox_size.isChecked():
                    self.sync_annotation_sizes_for_current_frame(selected)
            else:
                self.ti_view.set_annotation_enabled(False)
                self.rgb_view.set_annotation_enabled(False)
            
            # 确保原始分辨率设置正确应用
            if self.show_full_resolution:
                # 延迟执行适应操作，确保视图已经更新
                QTimer.singleShot(50, lambda: self.ti_view.fitInView(
                    self.ti_view.scene().sceneRect(), Qt.KeepAspectRatio))
            else:
                self.load_ti_frame(force_reload=False)
                
        else:
            self.current_single_mode = "RGB"
            self.ti_view.hide()
            self.rgb_view.show()
            self.toggle_modality_button.setText("显示TI")
            
            # 保存当前标注状态
            any_annotation_modified = self.annotation_modified
            if any_annotation_modified:
                self.save_annotations_to_disk()
            
            # 重置视图变换
            self.rgb_view.resetTransform()
            
            # 获取当前选择的颜色
            selected = self.annotation_button_group.checkedId()
            if selected != -1:
                btn = self.annotation_button_group.checkedButton()
                color_text = btn.styleSheet()
                if "color:" in color_text:
                    color = color_text.split("color:")[1].split(";")[0].strip()
                else:
                    color = self.color_map.get(selected, "#FF0000")
                    
                self.rgb_view.set_annotation_enabled(True)
                self.rgb_view.set_current_color(selected, color)
                self.ti_view.set_annotation_enabled(False)
                
                # 同步框大小设置
                if hasattr(self, 'sync_bbox_size') and self.sync_bbox_size.isChecked():
                    self.sync_annotation_sizes_for_current_frame(selected)
            else:
                self.rgb_view.set_annotation_enabled(False)
                self.ti_view.set_annotation_enabled(False)
            
            # 确保原始分辨率设置正确应用
            if self.show_full_resolution:
                # 延迟执行适应操作，确保视图已经更新
                QTimer.singleShot(50, lambda: self.rgb_view.fitInView(
                    self.rgb_view.scene().sceneRect(), Qt.KeepAspectRatio))
            else:
                self.load_rgb_frame(force_reload=False)
        
        # 确保切换模态后重新渲染标注
        self.redraw_all_annotations()
        self.log_message(f"已切换到{self.current_single_mode}模式")

        # 切换模态后确保toggle_modality_button按钮保持可见
        if not self.two_column_mode:
            self.toggle_modality_button.show()

    def sync_annotation_sizes_for_current_frame(self, color_id):
        """同步当前帧中指定颜色ID的RGB和TI标注框尺寸"""
        if not hasattr(self, 'sync_bbox_size') or not self.sync_bbox_size.isChecked():
            return
            
        # 查找RGB和TI标注
        rgb_ann = None
        ti_ann = None
        
        for ann in self.annotations:
            if ann.get("frame") == self.current_frame_index and ann.get("color_id") == color_id:
                if ann.get("modality") == "RGB":
                    rgb_ann = ann
                elif ann.get("modality") == "TI":
                    ti_ann = ann
        
        # 如果两种模态都有标注，同步它们的尺寸
        if rgb_ann and ti_ann:
            # 决定哪个标注作为尺寸基准
            source_ann = rgb_ann if self.current_single_mode == "RGB" else ti_ann
            target_ann = ti_ann if self.current_single_mode == "RGB" else rgb_ann
            
            # 获取源标注的尺寸
            source_width = source_ann.get("bbox")[2]
            source_height = source_ann.get("bbox")[3]
            
            # 更新目标标注的尺寸但保持其位置
            target_x = target_ann.get("bbox")[0]
            target_y = target_ann.get("bbox")[1]
            
            # 更新目标标注的bbox数据
            target_ann["bbox"] = [target_x, target_y, source_width, source_height]
            
            # 获取颜色信息
            color = self.color_map.get(color_id, "#FF0000")
            
            # 重新绘制带有更新尺寸的标注
            if self.current_single_mode == "RGB":
                # 在TI视图中重新绘制标注
                self.ti_view.clearColorAnnotation(color_id, modality="TI")
                rect = QRectF(target_x, target_y, source_width, source_height)
                self.ti_view.drawAnnotation(rect, color, color_id, modality="TI")
                self.log_message(f"已同步TI标注尺寸到RGB标注尺寸 {int(source_width)}×{int(source_height)}")
            else:
                # 要处理原始分辨率情况下的RGB标注
                self.rgb_view.clearColorAnnotation(color_id, modality="RGB")
                
                if self.show_full_resolution:
                    # 在原始分辨率模式下需要转换坐标
                    orig_coords = self.convert_annotation_coordinates(
                        [target_x, target_y, source_width, source_height], 
                        to_original=True)
                    rect = QRectF(*orig_coords)
                else:
                    rect = QRectF(target_x, target_y, source_width, source_height)
                    
                self.rgb_view.drawAnnotation(rect, color, color_id, modality="RGB")
                self.log_message(f"已同步RGB标注尺寸到TI标注尺寸 {int(source_width)}×{int(source_height)}")

    def load_current_frame(self, force_reload=False):
        """Load the current frame based on display mode and clear previous annotations"""
        # First clear any existing highlights
        if self.highlighted_item and not sip.isdeleted(self.highlighted_item):
            try:
                scene = self.highlighted_item.scene()
                if scene:
                    scene.removeItem(self.highlighted_item)
            except RuntimeError:
                pass
        self.highlighted_item = None
        
        # Clear any tree selection
        if hasattr(self, 'bbox_tree'):
            self.bbox_tree.clearSelection()
        
        # IMPORTANT: Clear all annotations before loading new frame
        self.annotations = []
        self.rgb_view.clearAnnotations()
        self.ti_view.clearAnnotations()
        
        # Load the appropriate images based on current display mode
        if self.two_column_mode:
            self.load_both_modalities(force_reload)
        else:
            if self.current_single_mode == "RGB":
                self.load_rgb_frame(force_reload)
            else:
                self.load_ti_frame(force_reload)
        
        # Load annotations from JSON file corresponding to current frame
        self.load_frame_annotations()
        
        # Update bbox tree based on the newly loaded annotations
        self.update_bbox_tree()

    def load_both_modalities(self, force_reload=False):
        """Load both RGB and TI frames"""
        rgb_path = self.rgb_frames[self.current_frame_index] if self.rgb_frames else None
        ti_path = self.ti_frames[self.current_frame_index] if self.ti_frames else None
        self._load_images(rgb_path, ti_path, force_reload)

    def load_rgb_frame(self, force_reload=False):
        """Load the RGB frame"""
        rgb_path = self.rgb_frames[self.current_frame_index] if self.rgb_frames else None
        self._load_images(rgb_path, None, force_reload)

    def load_ti_frame(self, force_reload=False):
        """Load the TI frame"""
        ti_path = self.ti_frames[self.current_frame_index] if self.ti_frames else None
        self._load_images(None, ti_path, force_reload)

    def _load_images(self, rgb_path, ti_path, force_reload=False):
        """Load images and update views"""
        self.rgb_view.scene().clear()
        self.ti_view.scene().clear()
        
        # 保存原图大小和经过处理后的图像大小，用于标注坐标映射
        self.original_rgb_size = None
        
        if (rgb_path and os.path.exists(rgb_path)):
            try:
                self.rgb = cv2.imread(rgb_path)
                if self.rgb is not None:
                    # 保存原始图像尺寸
                    self.original_rgb_size = (self.rgb.shape[1], self.rgb.shape[0])
                    
                    pixmap_rgb = None
                    
                    if self.show_full_resolution:
                        # 显示原始分辨率图像
                        pixmap_rgb = cv2_to_qpixmap(self.rgb)
                        # 不保存调整尺寸的副本，因为我们使用原始图像
                        self.rgb_resized = self.rgb.copy()
                        # 记录坐标映射信息 - 原始分辨率模式下无需缩放
                        self.scale_factor = 1.0
                        self.offset_x = 0
                        self.offset_y = 0
                    else:
                        # 标准流程，调整RGB尺寸以匹配TI尺寸
                        # 计算目标尺寸
                        if hasattr(self, 'ti') and self.ti is not None:
                            target_width, target_height = self.ti.shape[1], self.ti.shape[0]
                        else:
                            # 如果没有TI图像，则使用默认的目标尺寸
                            target_width, target_height = 640, 512  # 常见热成像分辨率
                        
                        # 调整大小并保存为rgb_resized
                        self.rgb_resized = center_crop_resize(self.rgb, target_width, target_height)
                        pixmap_rgb = cv2_to_qpixmap(self.rgb_resized)
                        
                        # 计算坐标映射因子
                        orig_w, orig_h = self.original_rgb_size
                        self.scale_factor = target_width / orig_w
                        
                        # 计算中心裁剪的偏移量
                        orig_aspect = orig_w / orig_h
                        target_aspect = target_width / target_height
                        
                        if orig_aspect > target_aspect:
                            # 原图更宽，上下会有裁剪
                            scaled_height = int(orig_h * (target_width / orig_w))
                            self.offset_y = (scaled_height - target_height) // 2
                            self.offset_x = 0
                        else:
                            # 原图更高，左右会有裁剪
                            scaled_width = int(orig_w * (target_height / orig_h))
                            self.offset_x = (scaled_width - target_width) // 2
                            self.offset_y = 0
                    
                    self.rgb_filename_edit.setText(os.path.basename(rgb_path))
                    self.rgb_filepath_edit.setText(rgb_path)
                    
                    # 更新场景
                    if pixmap_rgb is not None:
                        self.rgb_view.scene().addPixmap(pixmap_rgb)
                        self.rgb_view.scene().setSceneRect(QRectF(pixmap_rgb.rect()))
                else:
                    QMessageBox.warning(self, "错误", f"无法加载RGB图像: {rgb_path}")
            except Exception as e:
                logger.error(f"Error loading RGB image: {e}")
        else:
            self.rgb = None
            self.rgb_view.scene().clear()
            self.rgb_filename_edit.setText("N/A")
            self.rgb_filepath_edit.setText("N/A")
                
        # Load TI image if path provided - TI always loads at native resolution
        if (ti_path and os.path.exists(ti_path)):
            try:
                self.ti = cv2.imread(ti_path)
                if self.ti is None:
                    QMessageBox.warning(self, "错误", f"无法加载TI图像: {ti_path}")
                else:
                    pixmap_ti = cv2_to_qpixmap(self.ti)
                    self.ti_view.scene().clear()
                    self.ti_view.scene().addPixmap(pixmap_ti)
                    self.ti_view.scene().setSceneRect(QRectF(pixmap_ti.rect()))
                    self.ti_filename_edit.setText(os.path.basename(ti_path))
                    self.ti_filepath_edit.setText(ti_path)
            except Exception as e:
                logger.error(f"Error loading TI image: {e}")
        else:
            self.ti = None
            self.ti_view.scene().clear()
            self.ti_filename_edit.setText("N/A")
            self.ti_filepath_edit.setText("N/A")
                
        # Apply any offset and fit to view
        self.update_movement()
        
        # 即使在原始分辨率模式下也要适应视图大小
        self._auto_fit_views_for_full_res()
        
        self.update_frame_metadata()
        
        # Important: Load annotations for the frame after loading the images
        self.load_frame_annotations()
        
        # 强制重新绘制标注，解决第一帧标注不显示问题
        QTimer.singleShot(50, self.redraw_all_annotations)
        
        return True


    def _auto_fit_views_for_full_res(self):
        """在原始分辨率模式下自适应缩放视图，保持高质量但避免图像过大"""
        if self.show_full_resolution:
            # 原始分辨率模式下，我们希望适应视图但保持原始比例
            # 使用延时操作确保视图已更新
            QTimer.singleShot(10, self._delayed_fit_views)
        # 标准的自适应处理
        elif self.autoFit:
            self._auto_fit_views()
    
    def _delayed_fit_views(self):
        """延迟执行的适应视图操作，确保场景已正确设置"""
        if self.two_column_mode:
            if self.rgb_view.scene() and self.rgb_view.scene().sceneRect().isValid():
                self.rgb_view.fitInView(self.rgb_view.scene().sceneRect(), Qt.KeepAspectRatio)
            if self.ti_view.scene() and self.ti_view.scene().sceneRect().isValid():
                self.ti_view.fitInView(self.ti_view.scene().sceneRect(), Qt.KeepAspectRatio)
        else:
            current_view = self.rgb_view if self.current_single_mode == "RGB" else self.ti_view
            if current_view.scene() and current_view.scene().sceneRect().isValid():
                current_view.fitInView(current_view.scene().sceneRect(), Qt.KeepAspectRatio)
        
        # 设置滚动条策略
        self.rgb_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.rgb_view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.ti_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.ti_view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)

    def on_video_selected(self, item):
        """Handle video selection from the list"""
        if item.childCount() > 0:
            return

        # 清理预览状态，防止崩溃
        self.clean_up_preview_state()

        # 重要修改: 保存所有视频帧的标注，而不仅是当前帧
        if hasattr(self, 'cached_video_id') and self.cached_video_id:
            # 保存当前所有帧的标注
            self.save_all_annotations_to_disk()
            self.log_message("已保存所有帧的标注数据")

        # 清除视图中的标注显示
        self.rgb_view.clearAnnotations()
        self.ti_view.clearAnnotations()
        
        # 清除内存中的标注数据
        self.annotations = []
        if self.annotation_manager:
            self.annotation_manager.clear_workspace(self.current_project)
        
        self.offset_slider_x.setValue(0)
        self.offset_slider_y.setValue(0)

        # 获取路径和前缀（元组格式）
        data = item.data(0, Qt.UserRole)
        
        # 检查数据类型，支持新旧数据格式
        if isinstance(data, tuple):
            video_path, prefix = data  # 新格式：(路径, 前缀)
        else:
            video_path = data  # 旧格式：只有路径
            # 根据文件名确定前缀
            file_name = os.path.basename(video_path)
            if '_T.' in file_name.upper() or 'THERMAL' in file_name.upper():
                self.current_single_mode = "TI"
                prefix = "T: "
            else:
                self.current_single_mode = "RGB"
                prefix = "V: "

        file_name = os.path.basename(video_path)
        
        # 根据前缀设置当前模式
        if prefix.startswith("T:"):
            self.current_single_mode = "TI"
        else:
            self.current_single_mode = "RGB"
        
        # 更新项目显示，保留前缀
        item.setText(0, f"{prefix}{os.path.basename(video_path)} ✓")

        # 正确设置视图可见性
        if self.current_single_mode == "TI":
            self.rgb_view.hide()
            self.ti_view.show()
            self.toggle_modality_button.setText("显示RGB")
        else:
            self.ti_view.hide()
            self.rgb_view.show()
            self.toggle_modality_button.setText("显示TI")

        videos_dir = os.path.join(self.current_project, "Videos")
        try:
            rel_path = os.path.relpath(video_path, start=videos_dir)
        except ValueError:
            QMessageBox.warning(self, "路径错误", "视频文件不在项目目录内")
            return

        dir_part = os.path.dirname(rel_path)
        file_name = os.path.basename(video_path)

        parts = file_name.split('_')
        if len(parts) >= 4 and (parts[-1].lower().startswith(('t.', 'v.'))):
            group_key = '_'.join(parts[:3])
        else:
            group_key = os.path.splitext(file_name)[0]

        group_folder = os.path.join(
            self.current_project,
            "Frames",
            dir_part,
            group_key
        )

        if not os.path.exists(group_folder):
            QMessageBox.warning(self, "路径错误",
                f"未找到帧文件夹: {group_folder}\n"
                "预期路径结构: Frames/视频子目录/文件名组信息/")
            return

        # 获取并确定帧文件夹
        if os.path.isdir(group_folder):
            subdirs = [d for d in os.listdir(group_folder) if os.path.isdir(os.path.join(group_folder, d))]
            if subdirs:
                # 根据当前模态确定要使用的文件夹
                folder_letter = "T" if self.current_single_mode == "TI" else "V"
            
                # 尝试查找指定的模态文件夹
                if folder_letter in subdirs:
                    current_folder = os.path.join(group_folder, folder_letter)
                else:
                    # 如果找不到指定模态，则使用第一个子文件夹
                    current_folder = os.path.join(group_folder, subdirs[0])
            else:
                current_folder = group_folder
        else:
            QMessageBox.warning(self, "警告", f"未找到帧文件夹: {group_folder}")
            return

        frame_folder = current_folder

        metadata_file = os.path.join(frame_folder, "metadata.json")
        if os.path.exists(metadata_file):
            self.loaded_metadata = load_frame_metadata(metadata_file)
            video_name = self.loaded_metadata.get("video_name", "N/A")
            frame_count = len(self.loaded_metadata.get("frames", []))
            self.update_frame_metadata()
        else:
            self.loaded_metadata = None

        # 加载当前模态文件夹中的所有帧
        self.current_frames = sorted(glob.glob(os.path.join(frame_folder, "*.jpg")) +
                                glob.glob(os.path.join(frame_folder, "*.png")))
        if not self.current_frames:
            QMessageBox.warning(self, "警告", "该视频对应的帧文件夹为空！")
            return

        # 确定相对的另一模态
        pair_modality = "T" if folder_letter == "V" else "V"
        pair_folder = os.path.join(group_folder, pair_modality)
        self.pair_frames = sorted(glob.glob(os.path.join(pair_folder, "*.jpg")) +
                                glob.glob(os.path.join(pair_folder, "*.png")))
        
        # 根据当前模态和配对情况分配帧，处理帧数不一致的情况
        if self.current_frames and self.pair_frames:
            # 获取两组帧的数量
            current_frames_count = len(self.current_frames)
            pair_frames_count = len(self.pair_frames)
        
            # 只在帧数不一致时发出警告
            if current_frames_count != pair_frames_count:
                diff = abs(current_frames_count - pair_frames_count)
                more_frames = "可见光" if current_frames_count > pair_frames_count else "热成像"
                QMessageBox.warning(self, "注意", 
                    f"可见光与热成像帧数不一致！\n"
                    f"{more_frames}帧数量超出{diff}帧。\n"
                    f"V帧数: {current_frames_count if folder_letter == 'V' else pair_frames_count}\n"
                    f"T帧数: {current_frames_count if folder_letter == 'T' else pair_frames_count}\n"
                    f"将使用映射匹配方式处理不同数量的帧。")
        
            # 根据当前模态正确分配帧路径，不再截断为最小帧数
            if self.current_single_mode == "TI":
                self.ti_frames = self.current_frames
                self.rgb_frames = self.pair_frames
            else:
                self.rgb_frames = self.current_frames
                self.ti_frames = self.pair_frames
        else:
            # 如果只有当前模态的帧，进行合理分配
            if self.current_single_mode == "TI":
                self.ti_frames = self.current_frames
                self.rgb_frames = []
            else:
                self.rgb_frames = self.current_frames
                self.ti_frames = []

        # 重置帧索引并设置滑块
        self.current_frame_index = 0
        total_frames = max(len(self.rgb_frames), len(self.ti_frames))
        self.frame_slider.setRange(0, total_frames - 1 if total_frames > 0 else 0)
        self.frame_slider.setValue(0)
        self.frame_label.setText(f"当前帧: 1/{total_frames}")
        
        # 提取视频ID用于缓存标识
        video_id = os.path.basename(group_folder)
        self.cached_video_id = video_id
        
        # 重置标注缓存
        self.annotation_cache = {}
        
        # 重要: 只使用load_current_frame来加载第一帧，避免重复加载
        if total_frames > 0:
            # 设置为原始分辨率模式
            self.show_full_resolution = True
            self.full_resolution_button.setChecked(True)
        
            # 清除现有标注
            self.annotations = []
            self.rgb_view.clearAnnotations()
            self.ti_view.clearAnnotations()
        
            # 始终加载双模态，确保两种标注都能加载
            if self.rgb_frames and self.ti_frames:
                rgb_path = self.rgb_frames[0]
                ti_path = self.ti_frames[0]
                self.load_images(rgb_path, ti_path, keep_resolution_mode=True)
            elif self.rgb_frames:
                rgb_path = self.rgb_frames[0]
                self.load_images(rgb_path, None, keep_resolution_mode=True)
            elif self.ti_frames:
                ti_path = self.ti_frames[0]
                self.load_images(None, ti_path, keep_resolution_mode=True)
        else:
            self.clearAnnotations()
        
        # 更新项目显示    
        item.setText(0, f"{os.path.basename(video_path)} ✓")
        
        # 修改：直接加载标注，不使用计时器，避免TI标注加载失败
        self._auto_fit_views_for_full_res()
        
        # 确保同时加载RGB和TI的标注文件
        self.load_both_modality_annotations()
        
        # 渲染标注
        self.redraw_all_annotations()
        
        # 预加载视频所有标注
        QTimer.singleShot(500, self.preload_annotations_for_video)
        
        # 启用标注预览按钮
        if hasattr(self, 'preview_annotations_button'):
            self.preview_annotations_button.setEnabled(True)

    # 添加新的方法来保存所有标注
    def save_all_annotations_to_disk(self):
        """保存所有帧的标注到磁盘"""
        if not hasattr(self, 'cached_video_id') or not self.cached_video_id:
            return False
            
        # 找到RGB和TI的基础文件夹路径
        base_dir = None
        rgb_dir = None
        ti_dir = None
        
        # 获取基础目录结构
        if hasattr(self, 'rgb_frames') and self.rgb_frames and len(self.rgb_frames) > 0:
            rgb_path = self.rgb_frames[0]  # 使用第一帧作为参考
            if rgb_path and os.path.exists(rgb_path):
                dir_path = os.path.dirname(rgb_path)
                base_dir = os.path.dirname(dir_path)
                if os.path.basename(dir_path).upper() == "V":
                    rgb_dir = dir_path
                else:
                    potential_rgb_dir = os.path.join(base_dir, "V")
                    if os.path.exists(potential_rgb_dir):
                        rgb_dir = potential_rgb_dir
                    else:
                        rgb_dir = dir_path
        
        if hasattr(self, 'ti_frames') and self.ti_frames and len(self.ti_frames) > 0:
            ti_path = self.ti_frames[0]  # 使用第一帧作为参考
            if ti_path and os.path.exists(ti_path):
                dir_path = os.path.dirname(ti_path)
                if not base_dir:
                    base_dir = os.path.dirname(dir_path)
                if os.path.basename(dir_path).upper() == "T":
                    ti_dir = dir_path
                else:
                    potential_ti_dir = os.path.join(base_dir, "T")
                    if os.path.exists(potential_ti_dir):
                        ti_dir = potential_ti_dir
                    else:
                        ti_dir = dir_path
        
        if not base_dir:
            self.log_message("无法确定视频目录，无法保存标注")
            return False
        
        saved_count = 0
        
        # 收集内存中所有标注
        all_annotations = []
        
        # 1. 从当前标注列表中获取
        all_annotations.extend(self.annotations)
        
        # 2. 从缓存中获取所有标注
        for frame_idx, frame_anns in self.annotation_cache.items():
            # 确保不重复添加
            for ann in frame_anns:
                is_duplicate = False
                for existing_ann in all_annotations:
                    if (existing_ann.get("frame") == ann.get("frame") and
                        existing_ann.get("color_id") == ann.get("color_id") and
                        existing_ann.get("modality") == ann.get("modality")):
                        is_duplicate = True
                        break
                if not is_duplicate:
                    all_annotations.append(ann.copy())
        
        # 按模态分组
        rgb_annotations = [ann for ann in all_annotations if ann.get("modality") == "RGB"]
        ti_annotations = [ann for ann in all_annotations if ann.get("modality") == "TI"]
        
        # 保存RGB标注
        if rgb_dir and rgb_annotations:
            try:
                rgb_annotation_file = os.path.join(rgb_dir, "annotations.json")
                with open(rgb_annotation_file, 'w', encoding='utf-8') as f:
                    json.dump({"annotations": rgb_annotations}, f, ensure_ascii=False, indent=2)
                saved_count += len(rgb_annotations)
                self.log_message(f"已保存 {len(rgb_annotations)} 个RGB标注到 {rgb_dir}")
            except Exception as e:
                self.log_message(f"保存RGB标注出错: {str(e)}")
        
        # 保存TI标注
        if ti_dir and ti_annotations:
            try:
                ti_annotation_file = os.path.join(ti_dir, "annotations.json")
                with open(ti_annotation_file, 'w', encoding='utf-8') as f:
                    json.dump({"annotations": ti_annotations}, f, ensure_ascii=False, indent=2)
                saved_count += len(ti_annotations)
                self.log_message(f"已保存 {len(ti_annotations)} 个TI标注到 {ti_dir}")
            except Exception as e:
                self.log_message(f"保存TI标注出错: {str(e)}")
        
        # 重置修改标志
        self.annotation_modified = False
        
        return saved_count > 0

    def load_both_modality_annotations(self):
        """直接加载两种模态的标注文件，确保第一帧时两种标注都能加载"""
        if not hasattr(self, 'rgb_frames') and not hasattr(self, 'ti_frames'):
            return
        
        # 清除现有标注
        self.rgb_view.clearAnnotations()
        self.ti_view.clearAnnotations()
        self.annotations = []  # 清空当前的标注列表防止重复
        
        # 记录已加载的标注ID，用于去重
        loaded_annotations = {}  # 使用(frame, color_id, modality)作为键
        
        # 加载当前帧的RGB和TI标注
        annotations_loaded = False
        
        # 1. 尝试加载RGB标注
        if hasattr(self, 'rgb_frames') and self.rgb_frames and self.current_frame_index < len(self.rgb_frames):
            rgb_path = self.rgb_frames[self.current_frame_index]
            if rgb_path and os.path.exists(rgb_path):
                rgb_dir = os.path.dirname(rgb_path)
                rgb_annotation_file = os.path.join(rgb_dir, "annotations.json")
                
                if os.path.exists(rgb_annotation_file):
                    try:
                        with open(rgb_annotation_file, 'r', encoding='utf-8') as f:
                            data = json.load(f)
                        
                        if isinstance(data, dict) and "annotations" in data:
                            # 加载所有RGB标注，不仅仅是当前帧
                            rgb_annotations = data.get("annotations", [])
                            
                            # 添加到总标注列表中
                            for ann in rgb_annotations:
                                # 确保标注中有模态标记
                                if "modality" not in ann:
                                    ann["modality"] = "RGB"
                                    
                                # 创建唯一键以防重复
                                key = (ann.get("frame"), ann.get("color_id"), ann.get("modality"))
                                if key not in loaded_annotations:
                                    loaded_annotations[key] = True
                                    self.annotations.append(ann)
                            
                            # 更新缓存
                            if self.cached_video_id:
                                # 按帧更新缓存
                                for ann in rgb_annotations:
                                    frame_idx = ann.get("frame")
                                    if frame_idx not in self.annotation_cache:
                                        self.annotation_cache[frame_idx] = []
                                    
                                    # 避免重复
                                    is_duplicate = False
                                    for cached_ann in self.annotation_cache[frame_idx]:
                                        if (cached_ann.get("color_id") == ann.get("color_id") and 
                                            cached_ann.get("modality") == ann.get("modality")):
                                            is_duplicate = True
                                            break
                                    
                                    if not is_duplicate:
                                        self.annotation_cache[frame_idx].append(ann.copy())
                            
                            annotations_loaded = True
                            self.log_message(f"已加载RGB标注: {len(rgb_annotations)} 项")
                    except Exception as e:
                        self.log_message(f"加载RGB标注文件出错: {str(e)}")
        
        # 2. 尝试加载TI标注
        if hasattr(self, 'ti_frames') and self.ti_frames and self.current_frame_index < len(self.ti_frames):
            ti_path = self.ti_frames[self.current_frame_index]
            if ti_path and os.path.exists(ti_path):
                ti_dir = os.path.dirname(ti_path)
                ti_annotation_file = os.path.join(ti_dir, "annotations.json")
                
                if os.path.exists(ti_annotation_file):
                    try:
                        with open(ti_annotation_file, 'r', encoding='utf-8') as f:
                            data = json.load(f)
                        
                        if isinstance(data, dict) and "annotations" in data:
                            # 加载所有TI标注
                            ti_annotations = data.get("annotations", [])
                            
                            # 添加到总标注列表中，避免重复添加
                            for ann in ti_annotations:
                                # 确保标注中有模态标记
                                if "modality" not in ann:
                                    ann["modality"] = "TI"
                                    
                                # 创建唯一键以防重复
                                key = (ann.get("frame"), ann.get("color_id"), ann.get("modality"))
                                if key not in loaded_annotations:
                                    loaded_annotations[key] = True
                                    self.annotations.append(ann)
                            
                            # 更新缓存
                            if self.cached_video_id:
                                for ann in ti_annotations:
                                    frame_idx = ann.get("frame")
                                    if frame_idx not in self.annotation_cache:
                                        self.annotation_cache[frame_idx] = []
                                    
                                    # 避免重复
                                    is_duplicate = False
                                    for cached_ann in self.annotation_cache[frame_idx]:
                                        if (cached_ann.get("color_id") == ann.get("color_id") and 
                                            cached_ann.get("modality") == ann.get("modality")):
                                            is_duplicate = True
                                            break
                                    
                                    if not is_duplicate:
                                        self.annotation_cache[frame_idx].append(ann.copy())
                            
                            annotations_loaded = True
                            self.log_message(f"已加载TI标注: {len(ti_annotations)} 项")
                    except Exception as e:
                        self.log_message(f"加载TI标注文件出错: {str(e)}")
        
        # 3. 如果没有找到特定模态标注，尝试查找共享标注文件
        if not annotations_loaded:
            # 查找共享标注文件
            base_dir = None
            
            # 从RGB路径获取基础目录
            if hasattr(self, 'rgb_frames') and self.rgb_frames and self.current_frame_index < len(self.rgb_frames):
                rgb_path = self.rgb_frames[self.current_frame_index]
                if rgb_path and os.path.exists(rgb_path):
                    rgb_dir = os.path.dirname(rgb_path)
                    base_dir = os.path.dirname(rgb_dir)
            
            # 从TI路径获取基础目录
            if not base_dir and hasattr(self, 'ti_frames') and self.ti_frames and self.current_frame_index < len(self.ti_frames):
                ti_path = self.ti_frames[self.current_frame_index]
                if ti_path and os.path.exists(ti_path):
                    ti_dir = os.path.dirname(ti_path)
                    base_dir = os.path.dirname(ti_dir)
            
            # 如果找到基础目录，尝试加载共享标注
            if base_dir:
                shared_annotation_file = os.path.join(base_dir, "annotations.json")
                if os.path.exists(shared_annotation_file):
                    try:
                        with open(shared_annotation_file, 'r', encoding='utf-8') as f:
                            data = json.load(f)
                        
                        if isinstance(data, dict) and "annotations" in data:
                            frame_annotations = [ann for ann in data.get("annotations", [])]
                            
                            # 添加到总标注列表中，避免重复
                            for ann in frame_annotations:
                                if "modality" not in ann:
                                    # 如果没有模态信息，根据ID奇偶判断(或其他逻辑)
                                    ann["modality"] = "RGB" if ann.get("color_id", 0) % 2 == 0 else "TI"
                                
                                # 创建唯一键以防重复
                                key = (ann.get("frame"), ann.get("color_id"), ann.get("modality"))
                                if key not in loaded_annotations:
                                    loaded_annotations[key] = True
                                    self.annotations.append(ann)
                            
                            # 更新缓存
                            if self.cached_video_id:
                                for ann in frame_annotations:
                                    frame_idx = ann.get("frame")
                                    if frame_idx not in self.annotation_cache:
                                        self.annotation_cache[frame_idx] = []
                                    
                                    # 避免重复
                                    is_duplicate = False
                                    for cached_ann in self.annotation_cache[frame_idx]:
                                        if (cached_ann.get("color_id") == ann.get("color_id") and 
                                            cached_ann.get("modality") == ann.get("modality")):
                                            is_duplicate = True
                                            break
                                    
                                    if not is_duplicate:
                                        self.annotation_cache[frame_idx].append(ann.copy())
                            
                            self.log_message(f"已加载共享标注: {len(frame_annotations)} 项")
                    except Exception as e:
                        self.log_message(f"加载共享标注文件出错: {str(e)}")
        
        # 更新标注树
        self.update_bbox_tree()
        self.annotation_modified = False  # 重置修改标志
        
        # 检查重复并显示统计信息
        modality_count = {"RGB": 0, "TI": 0}
        # 仅统计当前帧的标注
        for ann in self.annotations:
            if ann.get("frame") == self.current_frame_index:
                modality = ann.get("modality", "Unknown")
                if modality in modality_count:
                    modality_count[modality] += 1
        
        self.log_message(f"标注加载完成 - 当前帧 RGB标注: {modality_count['RGB']}项, TI标注: {modality_count['TI']}项")
        
        # 延迟一段时间后重绘标注，确保标注正确显示
        QTimer.singleShot(50, self.redraw_all_annotations)

    def preload_annotations_for_video(self):
        """Preload annotations for the current video to improve navigation performance"""
        if not self.cached_video_id:
            return
            
        self.log_message("开始预加载标注...")
        self.is_loading_annotations = True
        
        try:
            # Find all annotation files in the video's frame folders
            annotation_files = []
            
            # Look in RGB frame folders
            rgb_dirs = set(os.path.dirname(path) for path in self.rgb_frames if path)
            for dir_path in rgb_dirs:
                ann_file = os.path.join(dir_path, "annotations.json")
                if os.path.exists(ann_file):
                    annotation_files.append(ann_file)
                    
            # Look in TI frame folders
            ti_dirs = set(os.path.dirname(path) for path in self.ti_frames if path)
            for dir_path in ti_dirs:
                ann_file = os.path.join(dir_path, "annotations.json")
                if os.path.exists(ann_file) and ann_file not in annotation_files:
                    annotation_files.append(ann_file)
                    
            # Also look in parent directories for shared annotations
            for dir_path in rgb_dirs.union(ti_dirs):
                parent_dir = os.path.dirname(dir_path)
                ann_file = os.path.join(parent_dir, "annotations.json")
                if os.path.exists(ann_file) and ann_file not in annotation_files:
                    annotation_files.append(ann_file)
            
            # Process all found annotation files
            frame_counts = 0
            annotation_counts = 0
            
            for ann_file in annotation_files:
                try:
                    with open(ann_file, 'r', encoding='utf-8') as f:
                        data = json.load(f)
                        
                    if isinstance(data, dict) and "annotations" in data:
                        # Group annotations by frame number
                        for ann in data["annotations"]:
                            if "frame" in ann:
                                frame = ann["frame"]
                                if frame not in self.annotation_cache:
                                    self.annotation_cache[frame] = []
                                
                                # Only add if this exact annotation isn't already in cache
                                existing = False
                                for cached_ann in self.annotation_cache[frame]:
                                    if (cached_ann.get("color_id") == ann.get("color_id") and
                                        cached_ann.get("modality") == ann.get("modality")):
                                        existing = True
                                        break
                                
                                if not existing:
                                    # Create a clean copy with just the essential fields
                                    clean_ann = {
                                        "frame": ann.get("frame"),
                                        "bbox": ann.get("bbox"),
                                        "color_id": ann.get("color_id"),
                                        "modality": ann.get("modality")
                                    }
                                    self.annotation_cache[frame].append(clean_ann)
                                    annotation_counts += 1
                                    
                                # Count unique frames
                                if len(self.annotation_cache[frame]) == 1:
                                    frame_counts += 1
                                    
                except Exception as e:
                    logger.error(f"预加载标注文件失败 {ann_file}: {str(e)}")
                    
            self.log_message(f"标注预加载完成: {frame_counts} 帧, {annotation_counts} 个标注")
            
        except Exception as e:
            logger.error(f"预加载标注过程出错: {str(e)}")
            self.log_message("预加载标注失败，将使用逐帧加载")
        finally:
            self.is_loading_annotations = False

    def update_frame_metadata(self):
        """
        Update the two metadata summary buttons with summary info.
        If in dual-modality mode, both buttons show data; otherwise, only the active modality is updated.
        """
        if not self.loaded_metadata:
            self.meta_summary_button.setText("无元数据")
            return
        frames = self.loaded_metadata.get("frames", [])
        if not frames or self.current_frame_index >= len(frames):
            self.meta_summary_button.setText("无当前帧")
            return
        frame_meta = frames[self.current_frame_index]
        md = frame_meta.get("metadata", {})
        
        # Generate camera ID - 使用原生哈希值
        cam_id = self.camera_id_mapper.get_camera_id(md)
        cam_id_str = f"场景ID: {cam_id}" if cam_id else "场景ID: 未知"
        
        summary = f"{cam_id_str}\nGPS: {md.get('latitude','')}, {md.get('longitude','')}\n海拔: {md.get('rel_alt','')}m/{md.get('abs_alt','')}m"
        full_info = "\n".join([
            f"场景ID: {cam_id}",
            f"Original Frame: {frame_meta.get('original_frame','')}",
            f"Video Timestamp: {frame_meta.get('video_timestamp','')} ",
            f"SRT Timestamp: {frame_meta.get('srt_timestamp','')} ",
            f"Diff Time: {frame_meta.get('diff_time','')} ",
            f"Camera: 焦距 {md.get('focal_len','')}mm, 缩放 {md.get('dzoom_ratio','')} ",
            f"GPS: {md.get('latitude','')}, {md.get('longitude','')} ",
            f"Relative Altitude: {md.get('rel_alt','')}m ",
            f"Absolute Altitude: {md.get('abs_alt','')}m ",
            f"Attitude: yaw {md.get('gb_yaw','')}, pitch {md.get('gb_pitch','')}, roll {md.get('gb_roll','')} "
        ])
        if self.two_column_mode:
            self.meta_summary_button.setText("元数据:\n" + summary)
            self._rgb_full_meta = full_info
            self._ti_full_meta = full_info
        else:
            if self.current_single_mode == "RGB":
                self.meta_summary_button.setText("RGB元数据:\n" + summary)
                self._rgb_full_meta = full_info
            else:
                self.meta_summary_button.setText("TI元数据:\n" + summary)
                self._ti_full_meta = full_info

    def show_full_meta_info(self):
        """Display a dialog showing full metadata details for both modalities"""
        dlg = QDialog(self)
        dlg.setWindowTitle("完整元信息")
        dlg.resize(500, 400)
        layout = QVBoxLayout(dlg)
        text_edit = QPlainTextEdit()
        text_edit.setReadOnly(True)
        if hasattr(self, 'rgb') and self.rgb is not None and hasattr(self, 'ti') and self.ti is not None:
            full_info = "RGB完整元信息:\n" + (self._rgb_full_meta if hasattr(self, "_rgb_full_meta") else "无元数据")
            full_info += "\n\nTI完整元信息:\n" + (self._ti_full_meta if hasattr(self, "_ti_full_meta") else "无元数据")
        else:
            if hasattr(self, 'rgb') and self.rgb is not None:
                full_info = "RGB完整元信息:\n" + (self._rgb_full_meta if hasattr(self, "_rgb_full_meta") else "无元数据")
            elif hasattr(self, 'ti') and self.ti is not None:
                full_info = "TI完整元信息:\n" + (self._ti_full_meta if hasattr(self, "_ti_full_meta") else "无元数据")
            else:
                full_info = "无元数据"
        text_edit.setPlainText(full_info)
        layout.addWidget(text_edit)
        close_btn = QPushButton("关闭")
        close_btn.clicked.connect(dlg.accept)
        layout.addWidget(close_btn)
        dlg.exec_()

    def next_frame(self):
        """加载下一帧图像，处理RGB/TI帧数不一致的情况"""
        # Save current frame annotations before changing
        if self.annotation_modified:
            self.save_annotations_to_disk()
            self.log_message("已保存当前帧标注")
            self.annotation_modified = False
        
        # Clear existing drawn annotations
        self.rgb_view.clearAnnotations()
        self.ti_view.clearAnnotations()
        self.drawn_annotations = {}  # Clear the tracking dictionary
        
        total_rgb = len(self.rgb_frames) if hasattr(self, "rgb_frames") and self.rgb_frames else 0
        total_ti = len(self.ti_frames) if hasattr(self, "ti_frames") and self.ti_frames else 0
        
        # 确定适合的下一个帧索引
        next_index = self.current_frame_index + 1
        
        # 检查是否已经到末尾
        if next_index >= max(total_rgb, total_ti):
            self.log_message("已经到达最后一帧")
            return
            
        # 更新当前帧索引
        self.current_frame_index = next_index
        
        # 根据当前帧索引获取适合的RGB和TI路径
        rgb_path = None
        ti_path = None
        
        if total_rgb > 0 and self.current_frame_index < total_rgb:
            rgb_path = self.rgb_frames[self.current_frame_index]
            
        if total_ti > 0 and self.current_frame_index < total_ti:
            ti_path = self.ti_frames[self.current_frame_index]
        
        # 如果某一模态已没有更多帧，保留显示最后一帧
        if not rgb_path and total_rgb > 0:
            rgb_path = self.rgb_frames[-1]
            self.log_message("RGB帧已播放完毕，保持显示最后一帧")
            
        if not ti_path and total_ti > 0:
            ti_path = self.ti_frames[-1]
            self.log_message("TI帧已播放完毕，保持显示最后一帧")
        
        # 确保使用原始分辨率加载
        self.show_full_resolution = True
        self.full_resolution_button.setChecked(True)
        
        # 加载帧
        self.load_images(rgb_path, ti_path, keep_resolution_mode=True)
        
        # 更新滑块位置
        self.frame_slider.setValue(self.current_frame_index)
        
        # 计算显示的总帧数
        total = max(total_rgb, total_ti)
        
        # 更新帧计数显示
        self.frame_label.setText(f"当前帧: {self.current_frame_index+1}/{total}")
        
        # 确保滚动条可见
        self.rgb_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.rgb_view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.ti_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.ti_view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        
        # Important: Load annotations for the new frame and update the display
        self.annotations = []  # Clear existing annotations
        self.rgb_view.clearAnnotations()  # Clear existing drawn annotations
        self.ti_view.clearAnnotations()
        self.load_frame_annotations()  # Load annotations for the current frame
        self.update_bbox_tree()  # Update the tree view
        
        # Ensure annotations get rendered
        if len(self.annotations) > 0:
            self.log_message(f"当前帧有 {len([a for a in self.annotations if a.get('frame') == self.current_frame_index])} 个标注")

    def prev_frame(self):
        """加载上一帧图像，处理RGB/TI帧数不一致的情况"""
        # Save current frame annotations before changing
        if self.annotation_modified:
            self.save_annotations_to_disk()
            self.log_message("已保存当前帧标注")
            self.annotation_modified = False
        
        # Clear existing drawn annotations
        self.rgb_view.clearAnnotations()
        self.ti_view.clearAnnotations()
        self.drawn_annotations = {}  # Clear the tracking dictionary
        
        # 检查是否已经是第一帧
        if self.current_frame_index <= 0:
            self.log_message("已经是第一帧")
            return
            
        # 更新当前帧索引
        self.current_frame_index -= 1
        
        total_rgb = len(self.rgb_frames) if hasattr(self, "rgb_frames") and self.rgb_frames else 0
        total_ti = len(self.ti_frames) if hasattr(self, "ti_frames") and self.ti_frames else 0
        
        # 根据当前帧索引获取适合的RGB和TI路径
        rgb_path = None
        ti_path = None
        
        if total_rgb > 0 and self.current_frame_index < total_rgb:
            rgb_path = self.rgb_frames[self.current_frame_index]
            
        if total_ti > 0 and self.current_frame_index < total_ti:
            ti_path = self.ti_frames[self.current_frame_index]
        
        # 如果某一模态已没有更多帧，保留显示最后一帧
        if not rgb_path and total_rgb > 0:
            rgb_path = self.rgb_frames[0]
            self.log_message("RGB帧已播放完毕，保持显示最后一帧")
            
        if not ti_path and total_ti > 0:
            ti_path = self.ti_frames[0]
            self.log_message("TI帧已播放完毕，保持显示最后一帧")
        
        # 确保使用原始分辨率加载
        self.show_full_resolution = True
        self.full_resolution_button.setChecked(True)
        
        # 加载帧
        self.load_images(rgb_path, ti_path, keep_resolution_mode=True)
        
        # 更新滑块位置
        self.frame_slider.setValue(self.current_frame_index)
        
        # 计算显示的总帧数
        total = max(total_rgb, total_ti)
        
        # 更新帧计数显示
        self.frame_label.setText(f"当前帧: {self.current_frame_index+1}/{total}")
        
        # 确保滚动条可见
        self.rgb_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.rgb_view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.ti_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.ti_view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        
        # Important: Load annotations for the new frame and update the display
        self.annotations = []  # Clear existing annotations
        self.rgb_view.clearAnnotations()  # Clear existing drawn annotations
        self.ti_view.clearAnnotations()
        self.load_frame_annotations()  # Load annotations for the current frame
        self.update_bbox_tree()  # Update the tree view

    def on_frame_slider_changed(self, value):
        """处理帧滑动条变化（支持单模态）"""
        # Auto-save current frame annotations before changing
        if hasattr(self, 'current_frame_index') and self.current_frame_index != value:
            if self.annotation_modified:
                self.save_annotations_to_disk()
                self.log_message("已保存当前帧标注")
                self.annotation_modified = False
                
        self.current_frame_index = value
        
        # 确保使用原始分辨率加载
        self.show_full_resolution = True
        self.full_resolution_button.setChecked(True)
        
        # 添加边界检查逻辑以确保索引有效
        rgb_frame_available = hasattr(self, "rgb_frames") and self.rgb_frames and self.current_frame_index < len(self.rgb_frames)
        ti_frame_available = hasattr(self, "ti_frames") and self.ti_frames and self.current_frame_index < len(self.ti_frames)
        
        if rgb_frame_available:
            # 当可以访问RGB帧时
            ti_path = self.ti_frames[self.current_frame_index] if ti_frame_available else None
            self.load_images(self.rgb_frames[self.current_frame_index], ti_path, keep_resolution_mode=True)
            total = len(self.rgb_frames)
        elif ti_frame_available:
            # 当只能访问TI帧时
            rgb_path = None  # 不使用可能超出范围的RGB帧
            self.load_images(rgb_path, self.ti_frames[self.current_frame_index], keep_resolution_mode=True)
            total = len(self.ti_frames)
        else:
            self.log_message(f"无法访问帧索引 {self.current_frame_index+1}，超出了可用帧的范围")
            return
        
        # 确保滚动条可见
        self.rgb_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.rgb_view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.ti_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.ti_view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        
        # Update frame counter display
        self.frame_label.setText(f"当前帧: {self.current_frame_index+1}/{total}")
        
        # Important: Load annotations for the new frame and update the display
        self.annotations = []  # Clear existing annotations
        self.rgb_view.clearAnnotations()  # Clear existing drawn annotations
        self.ti_view.clearAnnotations()
        self.load_frame_annotations()  # Load annotations for the current frame
        self.update_bbox_tree()  # Update the tree view


    def check_annotation_consistency(self):
        """检查RGB和TI标注的一致性，报告详细的不匹配信息"""
        if not self.annotations:
            self.log_message("无标注可供检查")
            return
        
        # 按帧号和ID分组所有标注
        frame_id_annotations = {}
        
        # 首先收集所有标注信息，按帧和ID组织
        for ann in self.annotations:
            frame = ann.get("frame")
            color_id = ann.get("color_id")
            modality = ann.get("modality")
            
            key = (frame, color_id)
            if key not in frame_id_annotations:
                frame_id_annotations[key] = {"RGB": None, "TI": None}
            
            frame_id_annotations[key][modality] = ann
        
        # 检查不匹配情况
        mismatches = []
        
        for (frame, color_id), modalities in frame_id_annotations.items():
            rgb_ann = modalities["RGB"]
            ti_ann = modalities["TI"]
            
            if rgb_ann and ti_ann:
                # 两个模态都有标注，检查尺寸是否匹配
                rgb_bbox = rgb_ann.get("bbox", [0, 0, 0, 0])
                ti_bbox = ti_ann.get("bbox", [0, 0, 0, 0])
                
                # 如果是原始分辨率模式，需要考虑到比例转换
                rgb_width, rgb_height = rgb_bbox[2], rgb_bbox[3]
                ti_width, ti_height = ti_bbox[2], ti_bbox[3]
                
                # 允许一定的误差（10%）
                width_diff = abs(rgb_width - ti_width) / max(rgb_width, ti_width) if max(rgb_width, ti_width) > 0 else 0
                height_diff = abs(rgb_height - ti_height) / max(rgb_height, ti_height) if max(rgb_height, ti_height) > 0 else 0
                
                if width_diff > 0.10 or height_diff > 0.10:
                    mismatches.append({
                        "type": "size_mismatch",
                        "frame": frame,
                        "color_id": color_id,
                        "rgb_size": (rgb_width, rgb_height),
                        "ti_size": (ti_width, ti_height)
                    })
            elif rgb_ann and not ti_ann:
                # 只有RGB标注
                mismatches.append({
                    "type": "missing_ti",
                    "frame": frame,
                    "color_id": color_id
                })
            elif ti_ann and not rgb_ann:
                # 只有TI标注
                mismatches.append({
                    "type": "missing_rgb",
                    "frame": frame,
                    "color_id": color_id
                })
        
        # 显示结果
        if not mismatches:
            self.log_message("所有标注一致性检查通过!")
            return True
        
        # 创建详细报告
        report = []
        report.append("=== 标注一致性检查报告 ===")
        
        # 按类型分组不匹配
        missing_ti = [m for m in mismatches if m["type"] == "missing_ti"]
        missing_rgb = [m for m in mismatches if m["type"] == "missing_rgb"]
        size_mismatch = [m for m in mismatches if m["type"] == "size_mismatch"]
        
        if missing_ti:
            report.append("\n▶ 缺少TI标注:")
            for m in missing_ti:
                report.append(f"  帧 {m['frame']+1}, ID {m['color_id']}: 有RGB标注但缺少TI标注")
        
        if missing_rgb:
            report.append("\n▶ 缺少RGB标注:")
            for m in missing_rgb:
                report.append(f"  帧 {m['frame']+1}, ID {m['color_id']}: 有TI标注但缺少RGB标注")
        
        if size_mismatch:
            report.append("\n▶ 尺寸不匹配:")
            for m in size_mismatch:
                rgb_w, rgb_h = m["rgb_size"]
                ti_w, ti_h = m["ti_size"]
                report.append(f"  帧 {m['frame']+1}, ID {m['color_id']}: RGB尺寸({int(rgb_w)}×{int(rgb_h)})与TI尺寸({int(ti_w)}×{int(ti_h)})不一致")
        
        # 显示报告对话框
        dialog = QDialog(self)
        dialog.setWindowTitle("标注一致性检查")
        layout = QVBoxLayout(dialog)
        
        text_edit = QTextEdit()
        text_edit.setReadOnly(True)
        text_edit.setPlainText("\n".join(report))
        layout.addWidget(text_edit)
        
        # 添加操作按钮
        btn_layout = QHBoxLayout()
        
        # 修复尺寸不匹配按钮
        fix_size_btn = QPushButton("修复尺寸不匹配")
        fix_size_btn.setEnabled(len(size_mismatch) > 0)
        fix_size_btn.clicked.connect(lambda: self.fix_annotation_size_mismatches(size_mismatch))
        btn_layout.addWidget(fix_size_btn)
        
        # 关闭按钮
        close_btn = QPushButton("关闭")
        close_btn.clicked.connect(dialog.accept)
        btn_layout.addWidget(close_btn)
        
        layout.addLayout(btn_layout)
        
        dialog.resize(600, 400)
        dialog.exec_()
        
        return False

    def fix_annotation_size_mismatches(self, mismatches):
        """修复RGB和TI标注的尺寸不匹配"""
        if not mismatches:
            return
        
        # 记录已修改的标注数量
        fixed_count = 0
        
        for mismatch in mismatches:
            frame = mismatch["frame"]
            color_id = mismatch["color_id"]
            
            # 查找RGB和TI标注
            rgb_ann = None
            ti_ann = None
            
            for ann in self.annotations:
                if ann.get("frame") == frame and ann.get("color_id") == color_id:
                    if ann.get("modality") == "RGB":
                        rgb_ann = ann
                    elif ann.get("modality") == "TI":
                        ti_ann = ann
            
            if rgb_ann and ti_ann:
                # 决定哪个尺寸作为基准（这里可以根据需要调整策略）
                # 默认使用RGB标注尺寸作为基准
                rgb_bbox = rgb_ann.get("bbox", [0, 0, 0, 0])
                ti_bbox = ti_ann.get("bbox", [0, 0, 0, 0])
                
                # 保留TI标注的位置，只同步尺寸
                ti_ann["bbox"] = [ti_bbox[0], ti_bbox[1], rgb_bbox[2], rgb_bbox[3]]
                fixed_count += 1
        
        # 重新绘制标注以应用修改
        self.redraw_all_annotations()
        
        # 更新缓存
        if self.cached_video_id:
            for frame in set(m["frame"] for m in mismatches):
                frame_annotations = [ann for ann in self.annotations if ann.get("frame") == frame]
                self.annotation_cache[frame] = frame_annotations.copy()
        
        # 保存修改到磁盘
        self.save_annotations_to_disk()
        
        # 显示结果
        self.log_message(f"已修复 {fixed_count} 个标注尺寸不匹配")

    def load_images(self, rgb_path, ti_path, keep_resolution_mode=False):
        """Load images from file paths and update the views"""
        # Clear current annotations
        self.rgb_view.clearAnnotations()
        self.ti_view.clearAnnotations()
        
        # Reset offsets
        self.offset_slider_x.setValue(0)
        self.offset_slider_y.setValue(0)
        
        # Load RGB image if path provided
        if rgb_path and os.path.exists(rgb_path):
            try:
                self.rgb = cv2.imread(rgb_path)
                if self.rgb is None:
                    logger.error(f"Could not load RGB image: {rgb_path}")
                else:
                    self.rgb_filename_edit.setText(os.path.basename(rgb_path))
                    self.rgb_filepath_edit.setText(rgb_path)
                    
                    # 保存原始图像尺寸
                    self.original_rgb_size = (self.rgb.shape[1], self.rgb.shape[0])
                    
                    # 如果需要保持原始分辨率模式，则重设标志
                    if keep_resolution_mode and self.show_full_resolution:
                        pixmap_rgb = cv2_to_qpixmap(self.rgb)
                        self.rgb_resized = self.rgb.copy()
                        self.scale_factor = 1.0
                        self.offset_x = 0
                        self.offset_y = 0
                        
                        self.rgb_view.scene().clear()
                        self.rgb_view.scene().addPixmap(pixmap_rgb)
                        self.rgb_view.scene().setSceneRect(QRectF(pixmap_rgb.rect()))
                        
                        # 确保滚动条可用
                        self.rgb_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
                        self.rgb_view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
            except Exception as e:
                logger.error(f"Error loading RGB image: {e}")
                self.rgb = None
        else:
            self.rgb = None
            self.rgb_filename_edit.setText("N/A")
            self.rgb_filepath_edit.setText("N/A")
            
        # Load TI image if path provided
        if ti_path and os.path.exists(ti_path):
            try:
                self.ti = cv2.imread(ti_path)
                if self.ti is None:
                    logger.error(f"Could not load TI image: {ti_path}")
                else:
                    self.ti_filename_edit.setText(os.path.basename(ti_path))
                    self.ti_filepath_edit.setText(ti_path)
                    
                    # TI总是以原始分辨率加载
                    pixmap_ti = cv2_to_qpixmap(self.ti)
                    self.ti_view.scene().clear()
                    self.ti_view.scene().addPixmap(pixmap_ti)
                    self.ti_view.scene().setSceneRect(QRectF(pixmap_ti.rect()))
                    
                    # 确保滚动条可用
                    self.ti_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
                    self.ti_view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
            except Exception as e:
                logger.error(f"Error loading TI image: {e}")
                self.ti = None
        else:
            self.ti = None
            self.ti_filename_edit.setText("N/A")
            self.ti_filepath_edit.setText("N/A")

        # 如果不是保持原始分辨率模式或不在原始分辨率模式下，则进行标准处理
        if not (keep_resolution_mode and self.show_full_resolution):
            # Resize images to match dimensions
            if self.rgb is not None and self.ti is not None:
                h = min(self.rgb.shape[0], self.ti.shape[0])
                w = min(self.rgb.shape[1], self.ti.shape[1])
                self.rgb_resized = center_crop_resize(self.rgb, w, h)
                self.ti_resized = center_crop_resize(self.ti, w, h)
            elif self.rgb is not None:
                self.rgb_resized = self.rgb.copy()
            elif self.ti is not None:
                self.ti_resized = self.ti.copy()
            else:
                # No images loaded
                return False
                
            # Update views with loaded images
            if self.rgb is not None and not (keep_resolution_mode and self.show_full_resolution):
                pixmap_rgb = cv2_to_qpixmap(self.rgb_resized)
                self.rgb_view.scene().clear()
                self.rgb_view.scene().addPixmap(pixmap_rgb)
                self.rgb_view.scene().setSceneRect(QRectF(pixmap_rgb.rect()))
                
            if self.ti is not None and not (keep_resolution_mode and self.show_full_resolution):
                pixmap_ti = cv2_to_qpixmap(self.ti_resized)
                self.ti_view.scene().clear()
                self.ti_view.scene().addPixmap(pixmap_ti)
                self.ti_view.scene().setSceneRect(QRectF(pixmap_ti.rect()))
            
        # Apply any offset and fit to view
        self.update_movement()
        
        # 根据当前模式决定是否自动适应视图
        if self.autoFit and not self.show_full_resolution:
            self._auto_fit_views()
        else:
            # 如果是原始尺寸或原始分辨率模式，确保滚动条可用
            self.rgb_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
            self.rgb_view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
            self.ti_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
            self.ti_view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        
        self.update_frame_metadata()
        
        # Important: Load annotations for the frame after loading the images
        self.load_frame_annotations()
        
        return True

    def load_project_images(self, project_path):
        frames_dir = os.path.join(project_path, "Frames")
        rgb_folder = ti_folder = None
        
    def set_project(self, project_path):
        self.current_project = project_path
        
        videos_dir = os.path.join(project_path, "Videos")
        if not os.path.exists(videos_dir):
            QMessageBox.warning(self, "警告", f"未找到视频目录: {videos_dir}")
            return
            
        video_files = []
        for root, _, files in os.walk(videos_dir):
            for f in files:
                if f.lower().endswith(('.mp4', '.mov')):
                    video_files.append(os.path.join(root, f))

        video_groups = {}
        for vf in video_files:
            base_name = os.path.basename(vf)
            group_key = "_".join(base_name.split("_")[:3])
            video_groups.setdefault(group_key, []).append(vf)

        self.video_list.clear()
        for group_key, videos in video_groups.items():
            group_item = QTreeWidgetItem([group_key])
            for vf in videos:
                # 确定视频类型（T或V）
                file_name = os.path.basename(vf)
                if '_T.' in file_name.upper() or 'THERMAL' in file_name.upper():
                    prefix = "T: "
                else:
                    prefix = "V: "
                    
                # 修改后
                video_item = QTreeWidgetItem([f"{prefix}{os.path.basename(vf)}"])
                video_item.setData(0, Qt.UserRole, (vf, prefix))  # 同时存储路径和前缀
                group_item.addChild(video_item)
            self.video_list.addTopLevelItem(group_item)
        self.video_list.expandAll()

    def getCurrentView(self):
        return self.rgb_view if self.current_single_mode == "RGB" else self.ti_view

    def wheelEvent(self, event):
        """Handle mouse wheel events for frame navigation"""
        # Get the vertical scroll amount
        delta = event.angleDelta().y()
        
        # Scroll up - go to previous frame
        if delta > 0:
            self.prev_frame()
        # Scroll down - go to next frame
        elif delta < 0:
            self.next_frame()
        
        # Accept the event to prevent it from being passed to parent widgets
        event.accept()
        
    def toggle_simultaneous_annotation(self, state):
        """控制是否启用同时标注模式"""
        self.simultaneous_annotation = (state == Qt.Checked)
        
        # 如果启用同时标注，则禁用同步框尺寸功能
        if self.simultaneous_annotation and hasattr(self, 'sync_bbox_size') and self.sync_bbox_size.isChecked():
            self.sync_bbox_size.blockSignals(True)
            self.sync_bbox_size.setChecked(False)
            self.sync_bbox_size.blockSignals(False)
            self.log_message("已禁用同步标注框尺寸功能（与同时标注冲突）")
        
        # 立即更新标注状态，根据当前选择的颜色ID
        if self.two_column_mode:
            # 确保两个视图都可以标注
            selected = self.annotation_button_group.checkedId()
            if selected != -1:
                btn = self.annotation_button_group.checkedButton()
                color = btn.styleSheet().split("color:")[1].split(";")[0].strip()
                
                # 双栏模式下，始终启用两个视图的标注功能
                self.rgb_view.set_annotation_enabled(True)
                self.ti_view.set_annotation_enabled(True)
                self.rgb_view.set_current_color(selected, color)
                self.ti_view.set_current_color(selected, color)
                
            # 显示当前状态的提示信息
            if self.simultaneous_annotation:
                self.log_message("已启用同时标注模式 - 在一个视图中绘制的标注将自动复制到另一个视图")
                self.log_message("提示：可使用X/Y偏移滑块微调标注同步的位置偏移")
            else:
                if self.sync_bbox_size.isChecked():
                    self.log_message("已禁用同时标注模式，但尺寸同步仍然启用")
                else:
                    self.log_message("已禁用同时标注模式")
        
    def add_or_update_annotation(self, new_ann: dict):
        selected = self.annotation_button_group.checkedId()
        if selected == -1:
            return
        btn = self.annotation_button_group.checkedButton()
        selected_color = btn.styleSheet().split("color:")[1].split(";")[0].strip()
        new_ann["reid_id"] = selected
        new_ann["color"] = selected_color

        for idx, ann in enumerate(self.annotations):
            if ann.get("frame_file") == new_ann.get("frame_file") and ann.get("reid_id") == selected:
                self.annotations[idx]["bbox_info"] = new_ann.get("bbox_info", ann.get("bbox_info"))
                self.annotations[idx]["metadata_info"] = new_ann.get("metadata_info", ann.get("metadata_info"))
                return
        self.annotations.append(new_ann)
    
    def save_annotations(self, file_path: str):
        data = {"annotations": self.annotations}
        os.makedirs(os.path.dirname(file_path), exist_ok=True)
        with open(file_path, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=4)
    
    def load_annotations(self, file_path: str):
        if not os.path.exists(file_path):
            return
        with open(file_path, 'r', encoding='utf-8') as f:
            data = json.load(f)
        self.annotations = data.get("annotations", [])
        
    def on_save_annotations(self):
        """保存标注到当前视图对应的文件夹，根据模态选择正确的路径"""
        if not self.rgb_filepath_edit.text() and not self.ti_filepath_edit.text():
            self.log_message("无有效的帧路径，无法保存标注")
            return
            
        if self.current_single_mode == "RGB":
            frame_dir = os.path.dirname(self.rgb_filepath_edit.text())
        else:
            frame_dir = os.path.dirname(self.ti_filepath_edit.text())
            
        file_path = os.path.join(frame_dir, "annotations.json")
        
        if self.auto_save_annotations():
            # 显示保存成功的消息
            folder_name = os.path.basename(os.path.dirname(file_path))
            modality = "RGB" if folder_name.upper() == "V" else "TI" if folder_name.upper() == "T" else "未知"
            self.log_message(f"已保存{modality}标注到 {os.path.basename(os.path.dirname(file_path))}")
        else:
            self.log_message("保存标注失败，请查看日志")

    def save_annotations_to_file(self, file_path, modality):
        """Save annotations to a specific file based on modality"""
        annotations = [ann for ann in self.annotations if ann["modality"] == modality]
        data = {"annotations": annotations}
        os.makedirs(os.path.dirname(file_path), exist_ok=True)
        with open(file_path, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=4)

    def on_annotation_selected(self, btn):
        """Handle color selection for annotation"""
        selected = self.annotation_button_group.checkedId()
        if selected != -1:
            btn = self.annotation_button_group.checkedButton()
            color = btn.styleSheet().split("color:")[1].split(";")[0].strip()
            
            # Reset any blocking states
            if hasattr(self, '_is_preview_open'):
                self._is_preview_open = False
            
            # Set current color for both views
            self.rgb_view.set_current_color(selected, color)
            self.ti_view.set_current_color(selected, color)
            
            # 在双栏模式下，总是启用两个模态的标注功能
            if self.two_column_mode:
                self.rgb_view.set_annotation_enabled(True)
                self.ti_view.set_annotation_enabled(True)
                self.log_message(f"已选择ID {selected}，RGB和TI视图均可标注")
            else:
                # 单栏模式保持原有逻辑
                if self.current_single_mode == "RGB":
                    self.rgb_view.set_annotation_enabled(True)
                    self.ti_view.set_annotation_enabled(False)
                    self.log_message(f"已选择ID {selected}，RGB视图可标注")
                else:
                    self.rgb_view.set_annotation_enabled(False)
                    self.ti_view.set_annotation_enabled(True)
                    self.log_message(f"已选择ID {selected}，TI视图可标注")
        else:
            self.rgb_view.set_annotation_enabled(False)
            self.ti_view.set_annotation_enabled(False)
            self.log_message("未选择标注颜色")

    def zoom_in(self):
        if self.two_column_mode:
            self.rgb_view.scale(1.2, 1.2)
            self.ti_view.scale(1.2, 1.2)
            self.autoFit = False
            self.rgb_view.autoFit = False
            self.ti_view.autoFit = False
        else:
            current_view = self.rgb_view if self.current_single_mode == "RGB" else self.ti_view
            current_view.scale(1.2, 1.2)
            self.autoFit = False
            current_view.autoFit = False

    def zoom_out(self):
        if self.two_column_mode:
            self.rgb_view.scale(0.8, 0.8)
            self.ti_view.scale(0.8, 0.8)
            self.autoFit = False
            self.rgb_view.autoFit = False
            self.ti_view.autoFit = False
        else:
            current_view = self.rgb_view if self.current_single_mode == "RGB" else self.ti_view
            current_view.scale(0.8, 0.8)
            self.autoFit = False
            current_view.autoFit = False
    def clearAnnotations(self):
        """清除所有视图中的标注框，并同步到磁盘"""
        # 清除两个视图中的所有标注
        self.rgb_view.clearAnnotations()
        self.ti_view.clearAnnotations()
        
        # 清除保存的标注引用
        if hasattr(self, 'drawn_annotations'):
            self.drawn_annotations = {}
        
        # 记住当前帧的索引，因为我们只清除当前帧的标注
        current_frame = self.current_frame_index
        
        # 从内存中删除当前帧的标注
        self.annotations = [ann for ann in self.annotations if ann.get("frame") != current_frame]
        
        # 如果使用annotation_manager，也从那里删除
        if self.annotation_manager and self.current_project:
            # 使用remove_annotation_by_attributes删除指定帧的所有标注
            self.annotation_manager.remove_annotation_by_attributes(
                self.current_project, 
                frame=current_frame
            )
        
        # 清空bbox树的显示
        if hasattr(self, 'bbox_tree'):
            self.bbox_tree.clear()
        
        # 清除高亮框
        if self.highlighted_item and not sip.isdeleted(self.highlighted_item):
            try:
                scene = self.highlighted_item.scene()
                if scene:
                    scene.removeItem(self.highlighted_item)
            except RuntimeError:
                pass
        self.highlighted_item = None
        
        # 将更改保存到annotations.json文件
        self.save_annotations_to_disk()
        
        self.log_message(f"已清除当前帧{current_frame + 1}的所有标注")
        
        # Mark that annotations have been modified    
        self.annotation_modified = True
        
        # Update the cache to reflect the cleared annotations
        if self.cached_video_id and self.current_frame_index is not None:
            if self.current_frame_index in self.annotation_cache:
                self.annotation_cache[self.current_frame_index] = []

    def update_annotation_info_tree(self):
        """更新右侧标注信息列表：显示bbox信息的简要描述，点击可查看详细信息"""
        self.annotation_info_tree.clear()
        # 假设 self.annotations 为所有标注的列表
        for ann in self.annotations:
            bbox = ann.get("bbox_info", {})
            # 创建简要描述
            summary = f"x:{bbox.get('x', '')}, y:{bbox.get('y', '')}, w:{bbox.get('width', '')}, h:{bbox.get('height', '')}"
            item = QTreeWidgetItem([summary])
            # 将详细内容保存在 UserRole 中（以 JSON 字符串存放）
            item.setData(0, Qt.UserRole, json.dumps(ann, ensure_ascii=False, indent=2))
            self.annotation_info_tree.addTopLevelItem(item)

    def show_annotation_details(self):
        """显示选中标注的详细信息"""
        selected_items = self.bbox_tree.selectedItems()
        if not selected_items:
            return
        item = selected_items[0]
        ann = item.data(0, Qt.UserRole)
        if not ann:
            return
        details = (
            f"ID: {ann['color_id']}\n"
            f"模态: {ann['modality']}\n"
            f"位置: x={ann['bbox'][0]:.1f}, y={ann['bbox'][1]:.1f}\n"
            f"尺寸: width={ann['bbox'][2]:.1f}, height={ann['bbox'][3]:.1f}\n"
            f"帧索引: {ann['frame']}"
        )
        QMessageBox.information(self, "标注详细信息", details)

    def _get_color_name(self, color_id):
        names = {
            1: "红色", 2: "绿色", 3: "蓝色", 4: "黄色", 5: "青色",
            6: "品红", 7: "橙色", 8: "紫色", 9: "棕色", 10: "灰色"
        }
        return names.get(color_id, f"颜色{color_id}")

    def deselect_annotation(self):
        """取消选择当前标注，清除高亮效果"""
        # 只删除高亮框，不影响其他标注
        if self.highlighted_item and not sip.isdeleted(self.highlighted_item):
            try:
                scene = self.highlighted_item.scene()
                if scene:
                    scene.removeItem(self.highlighted_item)  # 这里有一个bug，缺少removeItem调用
            except RuntimeError:
                pass
        
        # 重置高亮框引用
        self.highlighted_item = None
        
        # 清除树选择
        if hasattr(self, 'bbox_tree'):
            self.bbox_tree.clearSelection()
        
        # 禁用编辑控件
        self.disable_edit_controls()
        
        # 恢复标注状态
        selected = self.annotation_button_group.checkedId()
        if selected != -1:
            btn = self.annotation_button_group.checkedButton()
            color_text = btn.styleSheet()
            color = color_text.split("color:")[1].split(";")[0].strip() if "color:" in color_text else "#FF0000"
            
            # 重新启用视图标注功能
            if self.two_column_mode:
                self.rgb_view.set_annotation_enabled(True)
                self.ti_view.set_annotation_enabled(True)
                self.rgb_view.set_current_color(selected, color)
                self.ti_view.set_current_color(selected, color)
            else:
                if self.current_single_mode == "RGB":
                    self.rgb_view.set_annotation_enabled(True)  # 这里缺少代码
                    self.ti_view.set_annotation_enabled(False)
                    self.rgb_view.set_current_color(selected, color)
                else:
                    self.rgb_view.set_annotation_enabled(False)
                    self.ti_view.set_annotation_enabled(True)  # 这里缺少代码
                    self.ti_view.set_current_color(selected, color)
            
            self.log_message(f"已取消选择，可以使用ID {selected}重新标注")
        else:
            self.rgb_view.set_annotation_enabled(False)
            self.ti_view.set_annotation_enabled(False)
            self.log_message("当前标注：无")
        
        # 强制更新视图
        self.rgb_view.viewport().update()
        self.ti_view.viewport().update()

    def enable_all_annotations(self):
        """启用所有颜色的标注功能"""
        # Re-enable annotation on appropriate view based on mode
        selected = self.annotation_button_group.checkedId()
        if selected != -1:
            btn = self.annotation_button_group.checkedButton()
            color = btn.styleSheet().split("color:")[1].split(";")[0].strip()
            
            if self.current_single_mode == "RGB":
                self.rgb_view.set_annotation_enabled(True)
                self.ti_view.set_annotation_enabled(False)
            else:
                self.rgb_view.set_annotation_enabled(False)
                self.ti_view.set_annotation_enabled(True)
                
            self.rgb_view.set_current_color(selected, color)
            self.ti_view.set_current_color(selected, color)

    def disable_annotation_for_color(self, color_id):
        """禁用指定颜色的标注功能"""
        # Disable annotation for both views to prevent drawing for the selected color
        current_selected = self.annotation_button_group.checkedId()
        if current_selected == color_id:
            self.rgb_view.set_annotation_enabled(False)
            self.ti_view.set_annotation_enabled(False)

    def log_message(self, message):
        """Add message to the info log with timestamp"""
        if not hasattr(self, 'info_label'):
            return
            
        # If the widget is a QPlainTextEdit (new version)
        if isinstance(self.info_label, QPlainTextEdit):
            import datetime
            timestamp = datetime.datetime.now().strftime('%H:%M:%S')
            self.info_label.appendPlainText(f"[{timestamp}] {message}")
            self.info_label.verticalScrollBar().setValue(
                self.info_label.verticalScrollBar().maximum())
        else:
            # Backward compatibility for QLabel
            self.info_label.setText(message)

    def auto_save_annotations(self):
        """Auto-save annotations for the current frame, separated by modality"""
        # Check if we have frames loaded
        if not hasattr(self, 'rgb_frames') and not hasattr(self, 'ti_frames'):
            return False
            
        # Skip if no annotations to save
        if not self.annotations:
            return True
        
        # Separate annotations by modality
        rgb_annotations = []
        ti_annotations = []
        
        for ann in self.annotations:
            if ann.get("frame") == self.current_frame_index:
                if ann.get("modality") == "RGB":
                    rgb_annotations.append(ann)
                elif ann.get("modality") == "TI":
                    ti_annotations.append(ann)
        
        saved_count = 0
        
        # Save RGB annotations to V folder
        if rgb_annotations and hasattr(self, 'rgb_frames') and self.current_frame_index < len(self.rgb_frames):
            rgb_path = self.rgb_frames[self.current_frame_index]
            if rgb_path and os.path.exists(rgb_path):
                rgb_dir = os.path.dirname(rgb_path)
                
                # Check if we need to adjust directory to ensure we're in V folder
                dir_name = os.path.basename(rgb_dir).upper()
                if dir_name != 'V':
                    # Try to find/use V folder
                    parent_dir = os.path.dirname(rgb_dir)
                    v_dir = os.path.join(parent_dir, 'V')
                    if os.path.exists(v_dir) and os.path.isdir(v_dir):
                        rgb_dir = v_dir
                
                # Save RGB annotations
                try:
                    rgb_annotation_file = os.path.join(rgb_dir, "annotations.json")
                    existing_annotations = []
                    
                    if os.path.exists(rgb_annotation_file):
                        try:
                            with open(rgb_annotation_file, 'r', encoding='utf-8') as f:
                                data = json.load(f)
                            if isinstance(data, dict) and "annotations" in data:
                                existing_annotations = [ann for ann in data.get("annotations", []) 
                                                    if ann.get("frame") != self.current_frame_index]
                        except Exception as e:
                            logger.error(f"读取RGB标注文件出错: {e}")
                    
                    # Add new RGB annotations
                    existing_annotations.extend(rgb_annotations)
                    
                    # Save to file
                    with open(rgb_annotation_file, 'w', encoding='utf-8') as f:
                        json.dump({"annotations": existing_annotations}, f, ensure_ascii=False, indent=2)
                    
                    saved_count += len(rgb_annotations)
                    logger.debug(f"自动保存了 {len(rgb_annotations)} 个RGB标注到 {rgb_annotation_file}")
                except Exception as e:
                    logger.error(f"自动保存RGB标注出错: {e}")
        
        # Save TI annotations to T folder
        if ti_annotations and hasattr(self, 'ti_frames') and self.current_frame_index < len(self.ti_frames):
            ti_path = self.ti_frames[self.current_frame_index]
            if ti_path and os.path.exists(ti_path):
                ti_dir = os.path.dirname(ti_path)
                
                # Check if we need to adjust directory to ensure we're in T folder
                dir_name = os.path.basename(ti_dir).upper()
                if dir_name != 'T':
                    # Try to find/use T folder
                    parent_dir = os.path.dirname(ti_dir)
                    t_dir = os.path.join(parent_dir, 'T')
                    if os.path.exists(t_dir) and os.path.isdir(t_dir):
                        ti_dir = t_dir
                
                # Save TI annotations
                try:
                    ti_annotation_file = os.path.join(ti_dir, "annotations.json")
                    existing_annotations = []
                    
                    if os.path.exists(ti_annotation_file):
                        try:
                            with open(ti_annotation_file, 'r', encoding='utf-8') as f:
                                data = json.load(f)
                            if isinstance(data, dict) and "annotations" in data:
                                existing_annotations = [ann for ann in data.get("annotations", []) 
                                                    if ann.get("frame") != self.current_frame_index]
                        except Exception as e:
                            logger.error(f"读取TI标注文件出错: {e}")
                    
                    # Add new TI annotations
                    existing_annotations.extend(ti_annotations)
                    
                    # Save to file
                    with open(ti_annotation_file, 'w', encoding='utf-8') as f:
                        json.dump({"annotations": existing_annotations}, f, ensure_ascii=False, indent=2)
                    
                    saved_count += len(ti_annotations)
                    logger.debug(f"自动保存了 {len(ti_annotations)} 个TI标注到 {ti_annotation_file}")
                except Exception as e:
                    logger.error(f"自动保存TI标注出错: {e}")
        
        return saved_count > 0

    def save_annotations_to_disk(self):
        """保存标注到磁盘，区分RGB和TI模态，保留所有帧的标注"""
        if not hasattr(self, 'rgb_frames') and not hasattr(self, 'ti_frames'):
            return False
                
        # 找到RGB和TI的基础文件夹路径
        base_dir = None
        rgb_dir = None
        ti_dir = None
        
        # 从RGB或TI帧路径中找到基础文件夹
        if hasattr(self, 'rgb_frames') and self.rgb_frames and self.current_frame_index < len(self.rgb_frames):
            rgb_path = self.rgb_frames[self.current_frame_index]
            if rgb_path and os.path.exists(rgb_path):
                dir_path = os.path.dirname(rgb_path)
                base_dir = os.path.dirname(dir_path)  # 获取父目录
                # 检查是否已在V文件夹中
                if os.path.basename(dir_path).upper() == "V":
                    rgb_dir = dir_path
                else:
                    # 尝试查找V文件夹
                    potential_rgb_dir = os.path.join(base_dir, "V")
                    if os.path.exists(potential_rgb_dir):
                        rgb_dir = potential_rgb_dir
                    else:
                        rgb_dir = dir_path
        
        if hasattr(self, 'ti_frames') and self.ti_frames and self.current_frame_index < len(self.ti_frames):
            ti_path = self.ti_frames[self.current_frame_index]
            if ti_path and os.path.exists(ti_path):
                dir_path = os.path.dirname(ti_path)
                if not base_dir:
                    base_dir = os.path.dirname(dir_path)
                # 检查是否已在T文件夹中
                if os.path.basename(dir_path).upper() == "T":
                    ti_dir = dir_path
                else:
                    # 尝试查找T文件夹
                    potential_ti_dir = os.path.join(base_dir, "T")
                    if os.path.exists(potential_ti_dir):
                        ti_dir = potential_ti_dir
                    else:
                        ti_dir = dir_path
        
        # 如果找到了基础目录但没找到特定模态目录，创建它们
        if base_dir and not rgb_dir:
            rgb_dir = os.path.join(base_dir, "V")
            if not os.path.exists(rgb_dir):
                try:
                    os.makedirs(rgb_dir)
                except Exception as e:
                    self.log_message(f"创建RGB目录失败: {str(e)}")
        
        if base_dir and not ti_dir:
            ti_dir = os.path.join(base_dir, "T")
            if not os.path.exists(ti_dir):
                try:
                    os.makedirs(ti_dir)
                except Exception as e:
                    self.log_message(f"创建TI目录失败: {str(e)}")
        
        saved_count = 0
        
        # 保存RGB标注到V文件夹
        if rgb_dir:
            try:
                rgb_annotation_file = os.path.join(rgb_dir, "annotations.json")
                
                # 获取当前内存中的所有RGB标注
                all_rgb_annotations = [ann for ann in self.annotations if ann.get("modality") == "RGB"]
                
                # 从注释缓存中添加其他帧的RGB标注
                if hasattr(self, 'annotation_cache'):
                    for frame_index, frame_anns in self.annotation_cache.items():
                        if frame_index != self.current_frame_index:  # 跳过当前帧
                            for ann in frame_anns:
                                if ann.get("modality") == "RGB" and ann not in all_rgb_annotations:
                                    all_rgb_annotations.append(ann.copy())
                
                # 保存RGB标注
                rgb_data = {"annotations": all_rgb_annotations}
                with open(rgb_annotation_file, 'w', encoding='utf-8') as f:
                    json.dump(rgb_data, f, ensure_ascii=False, indent=2)
                    
                saved_count += len(all_rgb_annotations)
                self.log_message(f"已保存 {len(all_rgb_annotations)} 个RGB标注")
                    
            except Exception as e:
                self.log_message(f"保存RGB标注出错: {str(e)}")
        
        # 保存TI标注到T文件夹
        if ti_dir:
            try:
                ti_annotation_file = os.path.join(ti_dir, "annotations.json")
                
                # 获取当前内存中的所有TI标注
                all_ti_annotations = [ann for ann in self.annotations if ann.get("modality") == "TI"]
                
                # 从注释缓存中添加其他帧的TI标注
                if hasattr(self, 'annotation_cache'):
                    for frame_index, frame_anns in self.annotation_cache.items():
                        if frame_index != self.current_frame_index:  # 跳过当前帧
                            for ann in frame_anns:
                                if ann.get("modality") == "TI" and ann not in all_ti_annotations:
                                    all_ti_annotations.append(ann.copy())
                
                # 保存TI标注
                ti_data = {"annotations": all_ti_annotations}
                with open(ti_annotation_file, 'w', encoding='utf-8') as f:
                    json.dump(ti_data, f, ensure_ascii=False, indent=2)
                    
                saved_count += len(all_ti_annotations)
                self.log_message(f"已保存 {len(all_ti_annotations)} 个TI标注")
                    
            except Exception as e:
                self.log_message(f"保存TI标注出错: {str(e)}")
        
        # 更新缓存
        if self.cached_video_id and self.current_frame_index is not None:
            current_frame_annotations = [ann for ann in self.annotations if ann.get("frame") == self.current_frame_index]
            self.annotation_cache[self.current_frame_index] = current_frame_annotations.copy()
        
        # 重置修改标志
        self.annotation_modified = False
        
        return saved_count > 0

    def on_speed_changed(self, speed_text):
        """Handle speed selector changes and update playback timer if playing"""
        if hasattr(self, 'playing') and self.playing and self.timer:
            # Update the interval based on the new speed
            interval = self.get_frame_interval()
            self.timer.setInterval(interval)
            self.log_message(f"播放速度更改为 {speed_text}")

    def get_frame_interval(self):
        """Get timer interval in milliseconds based on selected speed"""
        speed_text = self.speed_selector.currentText()
        # Remove the 'x' and convert to float
        speed = float(speed_text.rstrip('x'))
        
        # Base interval is 100ms (10fps) at 1x speed
        base_interval = 100
        
        # Calculate interval - slower speed means longer interval
        interval = int(base_interval / speed)
        return interval

    def closeEvent(self, event):
        """确保在退出程序时保存当前标注"""
        # Auto-save annotations if modified
        if self.annotation_modified:
            self.save_annotations_to_disk()
            self.log_message("正在退出前保存标注...")
        
        # Call the parent class closeEvent
        super().closeEvent(event)

    def show_video_annotations_preview(self):
        """显示当前视频的所有标注预览，按帧号分组"""
        if hasattr(self, '_preview_lock') and self._preview_lock:
            self.log_message("标注预览窗口已打开，请等待操作完成")
            return

        self.clean_up_preview_state()   

        self._preview_lock = True
        
        selection_in_progress = False
        
        try:
            if not self.cached_video_id:
                self.log_message("未加载视频，无法显示标注预览")
                self._preview_lock = False
                return

            preview_dialog = QDialog(self)
            preview_dialog.setWindowTitle("视频标注预览")
            preview_dialog.resize(600, 500)
            layout = QVBoxLayout(preview_dialog)

            status_label = QLabel("正在加载标注预览...")
            layout.addWidget(status_label)

            annotations_tree = QTreeWidget()
            annotations_tree.setHeaderLabels(["帧号/ID", "标注信息", "模态"])
            annotations_tree.setColumnWidth(0, 80)
            annotations_tree.setColumnWidth(2, 60)
            layout.addWidget(annotations_tree)

            # 添加按钮区域
            btn_layout = QHBoxLayout()
            close_button = QPushButton("关闭")
            close_button.clicked.connect(preview_dialog.accept)
            refresh_button = QPushButton("刷新")
            refresh_button.clicked.connect(lambda: populate_tree())
            btn_layout.addWidget(refresh_button)
            btn_layout.addWidget(close_button)
            layout.addLayout(btn_layout)

            # 填充标注树的函数
            def populate_tree():
                annotations_tree.clear()
                status_label.setText("正在加载标注数据...")
                annotations_tree.setEnabled(False)
                
                # 从所有帧中收集标注
                all_annotations = []
                
                # 1. 从缓存加载
                for frame_idx, anns in self.annotation_cache.items():
                    for ann in anns:
                        # 确保是干净的副本
                        ann_copy = ann.copy()
                        ann_copy["type"] = "annotation"  # 标记为标注项
                        all_annotations.append(ann_copy)
                
                # 2. 从当前内存状态加载(以防有未缓存的内容)
                for ann in self.annotations:
                    # 跳过已经在缓存中的
                    is_duplicate = False
                    for existing_ann in all_annotations:
                        if (existing_ann.get("frame") == ann.get("frame") and
                            existing_ann.get("color_id") == ann.get("color_id") and
                            existing_ann.get("modality") == ann.get("modality")):
                            is_duplicate = True
                            break
                    
                    if not is_duplicate:
                        ann_copy = ann.copy()
                        ann_copy["type"] = "annotation"
                        all_annotations.append(ann_copy)
                
                # 按帧号分组
                annotation_by_frame = {}
                for ann in all_annotations:
                    frame = ann.get("frame")
                    if frame not in annotation_by_frame:
                        annotation_by_frame[frame] = []
                    annotation_by_frame[frame].append(ann)
                
                # 按帧号排序
                sorted_frames = sorted(annotation_by_frame.keys())
                
                # 变量跟踪是否有任何帧被添加
                has_frames = False
                
                # 生成树结构
                for frame in sorted_frames:
                    has_frames = True
                    frame_item = QTreeWidgetItem([f"帧 {frame+1}"])
                    frame_item.setData(0, Qt.UserRole, {"type": "frame", "frame": frame})
                    annotations_tree.addTopLevelItem(frame_item)
                    
                    # 添加此帧的所有标注
                    for ann in annotation_by_frame[frame]:
                        color_id = ann.get("color_id")
                        bbox = ann.get("bbox", [0, 0, 0, 0])
                        modality = ann.get("modality", "Unknown")
                        
                        # 创建子项
                        info_text = f"ID {color_id}: x={int(bbox[0])},y={int(bbox[1])},w={int(bbox[2])},h={int(bbox[3])}"
                        
                        ann_item = QTreeWidgetItem([f"ID {color_id}", info_text, modality])
                        ann_item.setData(0, Qt.UserRole, ann)
                        
                        # 设置背景色与标注颜色相符
                        color = self.color_map.get(color_id, "#888888")
                        for i in range(3):
                            ann_item.setBackground(i, QColor(color).lighter(150))
                        
                        frame_item.addChild(ann_item)
                
                # 更新状态
                total_frames = len(sorted_frames)
                total_annotations = len(all_annotations)
                status_label.setText(f"共 {total_frames} 帧有标注，总计 {total_annotations} 个标注")
                annotations_tree.setEnabled(True)
                
                # 只有当有帧时才展开第一个帧
                if has_frames:
                    # 获取第一个顶级项并展开
                    first_frame_item = annotations_tree.topLevelItem(0)
                    if first_frame_item:
                        first_frame_item.setExpanded(True)
                
                # 如果没有标注，显示提示
                if total_annotations == 0:
                    status_label.setText("当前视频没有标注")
                
            # 在打开对话框时调用填充函数
            QTimer.singleShot(100, populate_tree)

            # Inside the preview dialog, modify on_annotation_selected
            def on_annotation_selected(item, column):
                nonlocal selection_in_progress
                if selection_in_progress:
                    status_label.setText("正在处理之前的选择，请稍候...")
                    return
                # Disable further clicks until processing completes
                annotations_tree.setEnabled(False)
                try:
                    selection_in_progress = True
                    status_label.setText("正在处理选择...")
                    
                    data = item.data(0, Qt.UserRole)
                    if not data:
                        selection_in_progress = False
                        annotations_tree.setEnabled(True)
                        return
                            
                    if data.get("type") == "annotation":
                        frame_num = data.get("frame")
                        color_id = data.get("color_id")
                        modality = data.get("modality")
                        
                        # Update status
                        status_label.setText(f"正在跳转到帧 {frame_num+1}，{modality}标注 ID{color_id}...")
                        
                        # Make sure we cancel any pending or completed preview jumps
                        if hasattr(self, '_preview_jump_timer'):
                            if self._preview_jump_timer.isActive():
                                self._preview_jump_timer.stop()
                        
                        # Disable all color buttons to prevent drawing new annotations while jumping
                        for button_id in range(1, 11):  # Assuming color IDs 1-10
                            btn = self.annotation_button_group.button(button_id)
                            if btn and btn.isChecked():
                                btn.setAutoExclusive(False)
                                btn.setChecked(False)
                                btn.setAutoExclusive(True)
                        
                        self.rgb_view.set_annotation_enabled(False)
                        self.ti_view.set_annotation_enabled(False)
                        
                        # Navigate to the frame
                        if frame_num != self.current_frame_index:
                            # Save current annotations if needed
                            if self.annotation_modified:
                                self.save_annotations_to_disk()
                            
                            # Store the new frame index for safer navigation
                            target_frame = frame_num
                            
                            # Find paths for the new frame
                            rgb_path = self.rgb_frames[target_frame] if len(self.rgb_frames) > target_frame else None
                            ti_path = self.ti_frames[target_frame] if len(self.ti_frames) > target_frame else None
                            
                            # First close the dialog (safer to do this before switching frames)
                            preview_dialog.accept()
                            
                            # Set frame index directly first
                            self.current_frame_index = frame_num
                            self.frame_slider.setValue(frame_num)
                            
                            # Create a timer that we can track and cancel if needed
                            self._preview_jump_timer = QTimer()
                            self._preview_jump_timer.setSingleShot(True)
                            self._preview_jump_timer.timeout.connect(
                                lambda: self.load_frame_and_select_annotation(rgb_path, ti_path, frame_num, color_id, modality))
                            # Schedule the frame navigation
                            self._preview_jump_timer.start(100)
                            return
                        else:
                            # Same frame, just select the annotation
                            preview_dialog.accept()
                            QTimer.singleShot(100, lambda: self.log_message(f"已定位到帧 {frame_num+1} 的 {modality}标注 ID{color_id}"))
                    else:
                        # Re-enable the tree if not handling an annotation selection
                        annotations_tree.setEnabled(True)
                        selection_in_progress = False
                
                except Exception as e:
                    import traceback
                    self.log_message(f"选择标注时出错: {str(e)}")
                    status_label.setText(f"错误: {str(e)}")
                    print(traceback.format_exc())
                    annotations_tree.setEnabled(True)
                    selection_in_progress = False
                    self._preview_lock = False

            annotations_tree.itemClicked.connect(on_annotation_selected)
            preview_dialog.finished.connect(lambda: setattr(self, '_preview_lock', False))
            preview_dialog.exec_()
        except Exception as e:
            self._preview_lock = False
            self.log_message(f"显示标注预览出错: {str(e)}")
            import traceback
            print(traceback.format_exc())
            
    # Add new helper methods for safer frame navigation and annotation selection

    def load_frame_and_select_annotation(self, rgb_path, ti_path, frame_num, color_id, modality):
        """加载帧但不自动选中标注，只进行跳转"""
        try:
            # 只加载图像，不选择标注
            self.load_images_safely(rgb_path, ti_path)
            
            # 切换到正确的模态视图，但不选中标注
            if not self.two_column_mode and self.current_single_mode != modality:
                self.log_message(f"切换到{modality}模态以显示标注")
                if modality == "RGB" and self.current_single_mode != "RGB":
                    self.current_single_mode = "RGB"
                    self.rgb_view.show()
                    self.ti_view.hide()
                    self.toggle_modality_button.setText("显示TI")
                elif modality == "TI" and self.current_single_mode != "TI":
                    self.current_single_mode = "TI"
                    self.ti_view.show()
                    self.rgb_view.hide()
                    self.toggle_modality_button.setText("显示RGB")
                
                # 重新渲染标注
                self.redraw_all_annotations()

            # 显示提示信息，但不选中标注
            self.log_message(f"已跳转到帧 {frame_num+1}，包含{modality}标注 ID{color_id}")
            
            # 确保标注功能启用
            self.restore_annotation_state()
            
        except Exception as e:
            self.log_message(f"加载图像出错: {str(e)}")
        finally:
            # 释放预览锁
            if hasattr(self, '_preview_lock'):
                self._preview_lock = False

    def select_annotation_after_jump(self, color_id, modality):
        """Safely select an annotation after frame navigation"""
        try:
            # 取消所有颜色按钮的选择
            self.annotation_button_group.setExclusive(False)
            for btn in self.annotation_button_group.buttons():
                btn.setChecked(False)
            self.annotation_button_group.setExclusive(True)
            
            # 禁用标注功能
            self.rgb_view.set_annotation_enabled(False)
            self.ti_view.set_annotation_enabled(False)
            # Switch to the correct modality view if needed and not in two-column mode
            if not self.two_column_mode and self.current_single_mode != modality:
                self.log_message(f"切换到{modality}模态以显示标注")
                if modality == "RGB" and self.current_single_mode != "RGB":
                    self.current_single_mode = "RGB"
                    self.rgb_view.show()
                    self.ti_view.hide()
                    self.toggle_modality_button.setText("显示TI")
                elif modality == "TI" and self.current_single_mode != "TI":
                    self.current_single_mode = "TI"
                    self.ti_view.show()
                    self.rgb_view.hide()
                    self.toggle_modality_button.setText("显示RGB")
                
                # Re-render annotations for the newly visible view
                self.redraw_all_annotations()
            
            # Find the annotation in the tree and select it
            found = False
            for i in range(self.bbox_tree.topLevelItemCount()):
                item = self.bbox_tree.topLevelItem(i)
                ann_data = item.data(0, Qt.UserRole)
                if ann_data and ann_data.get("color_id") == color_id and ann_data.get("modality") == modality:
                    self.bbox_tree.setCurrentItem(item)
                    found = True
                    break
                    
            if found:
                # The tree selection will trigger highlighting
                self.log_message(f"已定位到帧 {self.current_frame_index+1} 的 {modality}标注 ID{color_id}")
                # Disable annotation for this color to prevent accidental overwriting
                self.disable_annotation_for_color(color_id)
                
                # Set a flag to indicate this annotation is from preview selection
                setattr(self, '_preview_selected_annotation', 
                        {'color_id': color_id, 'modality': modality, 'frame': self.current_frame_index})
            else:
                self.log_message(f"在当前帧未找到 {modality}标注 ID{color_id}")
        except Exception as e:
            self.log_message(f"选择标注出错: {str(e)}")
        finally:
            # Always make sure we release the lock
            self._preview_lock = False

    def load_images_safely(self, rgb_path, ti_path):
        """Load images without clearing annotations"""
        # Reset offsets
        self.offset_slider_x.setValue(0)
        self.offset_slider_y.setValue(0)
        
        # Load RGB image if path provided
        if rgb_path and os.path.exists(rgb_path):
            try:
                self.rgb = cv2.imread(rgb_path)
                if self.rgb is None:
                    logger.error(f"Could not load RGB image: {rgb_path}")
                else:
                    self.rgb_filename_edit.setText(os.path.basename(rgb_path))
                    self.rgb_filepath_edit.setText(rgb_path)
                    # Create pixmap and update view
                    pixmap_rgb = cv2_to_qpixmap(self.rgb)
                    self.rgb_view.scene().clear()
                    self.rgb_view.scene().addPixmap(pixmap_rgb)
                    self.rgb_view.scene().setSceneRect(QRectF(pixmap_rgb.rect()))
            except Exception as e:
                logger.error(f"Error loading RGB image: {e}")
                self.rgb = None
        
        # Load TI image if path provided
        if ti_path and os.path.exists(ti_path):
            try:
                self.ti = cv2.imread(ti_path)
                if self.ti is None:
                    logger.error(f"Could not load TI image: {ti_path}")
                else:
                    self.ti_filename_edit.setText(os.path.basename(ti_path))
                    self.ti_filepath_edit.setText(ti_path)
                    # Create pixmap and update view
                    pixmap_ti = cv2_to_qpixmap(self.ti)
                    self.ti_view.scene().clear()
                    self.ti_view.scene().addPixmap(pixmap_ti)
                    self.ti_view.scene().setSceneRect(QRectF(pixmap_ti.rect()))
            except Exception as e:
                logger.error(f"Error loading TI image: {e}")
                self.ti = None
                
        # Apply any offset and fit to view
        self.update_movement()
        self._auto_fit_views()
        self.update_frame_metadata()
        
        # Load annotations for the frame after loading the images
        self.load_frame_annotations()
        self.update_bbox_tree()
        
        return True

    def resizeEvent(self, event):
        """处理窗口大小变化事件"""
        super().resizeEvent(event)
        # 如果是适应视图模式，则调整图像大小
        if self.autoFit:
            QTimer.singleShot(0, self._auto_fit_views)
    
    def toggle_original_size(self):
        """切换图像显示模式：原始尺寸(1:1)或适应视图大小"""
        is_original = self.original_size_button.isChecked()
        
        # 不能同时启用原始尺寸和原始分辨率
        if is_original and self.full_resolution_button.isChecked():
            self.full_resolution_button.blockSignals(True)
            self.full_resolution_button.setChecked(False)
            self.full_resolution_button.blockSignals(False)
            self.show_full_resolution = False
            # 重新加载当前帧
            self.load_current_frame(force_reload=True)
        
        # 设置显示模式
        if (is_original):
            self.autoFit = False
            self.rgb_view.autoFit = False
            self.ti_view.autoFit = False
            self.original_size_button.setText("适应视图")
            
            # 应用1:1缩放（重置变换）
            self.rgb_view.resetTransform()
            if self.two_column_mode:
                self.ti_view.resetTransform()
            
            # 启用滚动条
            self.rgb_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
            self.rgb_view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
            self.ti_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
            self.ti_view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
            
            self.log_message("已切换到1:1原始尺寸视图")
        else:
            self.autoFit = True
            self.rgb_view.autoFit = True
            self.ti_view.autoFit = True
            self.original_size_button.setText("1:1原始尺寸")
            
            # 自动适应视图
            self._auto_fit_views()
            
            # 禁用滚动条
            self.rgb_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
            self.rgb_view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
            self.ti_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
            self.ti_view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
            
            self.log_message("已切换到适应视图大小模式")
        
        # 重新绘制标注以确保它们正确显示
        self.redraw_all_annotations()
    
    def toggle_full_resolution(self):
        """切换是否显示原始分辨率RGB图像"""
        # 更新状态
        self.show_full_resolution = self.full_resolution_button.isChecked()
        
        if self.show_full_resolution:
            # 启用原始分辨率模式
            self.full_resolution_button.setText("适应视图")
            
            # 设置滚动条策略以支持大图
            self.rgb_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
            self.rgb_view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
            self.ti_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
            self.ti_view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
            
            # 保存当前标注到内存
            if self.annotation_modified:
                self.save_annotations_to_disk()
            
            # 重新加载当前帧以原始分辨率显示
            self.load_current_frame(force_reload=True)
            
            # 确保标注正确显示
            self.redraw_all_annotations()
            
            self.log_message("已切换到原始分辨率模式")
        else:
            # 禁用原始分辨率模式
            self.full_resolution_button.setText("原始分辨率")
            
            # 启用自动适应
            self.autoFit = True
            self.rgb_view.autoFit = True
            self.ti_view.autoFit = True
            self.rgb_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
            self.rgb_view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
            self.ti_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
            self.ti_view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
            
            # 保存当前标注到内存
            if self.annotation_modified:
                self.save_annotations_to_disk()
            
            # 重新加载当前帧
            self.load_current_frame(force_reload=True)
            
            # 确保标注正确显示
            self.redraw_all_annotations()
            
            self.log_message("已切换到标准分辨率模式")
    
    def convert_annotation_coordinates(self, bbox, to_original=False):
        """在原始分辨率和缩放分辨率之间转换标注坐标
        
        Args:
            bbox: 标注框坐标 [x, y, width, height]
            to_original: True表示从缩放坐标转为原始坐标，False表示从原始坐标转为缩放坐标
            
        Returns:
            转换后的坐标 [x, y, width, height]
        """
        if not hasattr(self, 'original_rgb_size') or self.original_rgb_size is None:
            return bbox
            
        x, y, width, height = bbox
        
        if to_original:
            # 从缩放坐标转为原始坐标
            if self.scale_factor > 0:
                orig_x = (x + self.offset_x) / self.scale_factor
                orig_y = (y + self.offset_y) / self.scale_factor
                orig_width = width / self.scale_factor
                orig_height = height / self.scale_factor
                return [orig_x, orig_y, orig_width, orig_height]
        else:
            # 从原始坐标转为缩放坐标
            if self.scale_factor > 0:
                scaled_x = x * self.scale_factor - self.offset_x
                scaled_y = y * self.scale_factor - self.offset_y
                scaled_width = width * self.scale_factor
                scaled_height = height * self.scale_factor
                return [scaled_x, scaled_y, scaled_width, scaled_height]
            
        return bbox


    def disable_shortcuts(self):
        """禁用所有键盘快捷键"""
        self.shortcuts_enabled = False
        for shortcut in self.shortcuts:
            shortcut.setEnabled(False)
            
    def enable_shortcuts(self):
        """启用所有键盘快捷键"""
        self.shortcuts_enabled = True
        for shortcut in self.shortcuts:
            shortcut.setEnabled(True)
        self.log_message("快捷键已启用")

    def _setup_shortcuts(self):
        """设置所有快捷键，但默认不激活"""
        # 空格键 - 下一帧 (修改)
        self.shortcuts.append(QShortcut(QKeySequence(Qt.Key_Space), self))
        self.shortcuts[-1].activated.connect(self.next_frame)
        
        # P键 - 播放/暂停视频 (新增)
        self.shortcuts.append(QShortcut(QKeySequence("P"), self))
        self.shortcuts[-1].activated.connect(self.toggle_play)
    
        
        # 左右方向键 - 上一帧/下一帧
        self.shortcuts.append(QShortcut(QKeySequence(Qt.Key_Left), self))
        self.shortcuts[-1].activated.connect(self.prev_frame)
        self.shortcuts.append(QShortcut(QKeySequence(Qt.Key_Right), self))
        self.shortcuts[-1].activated.connect(self.next_frame)
        
        # D键 - 删除当前选中的标注框
        self.shortcuts.append(QShortcut(QKeySequence("D"), self))
        self.shortcuts[-1].activated.connect(self.delete_selected_annotation)
        
        # S键 - 保存当前标注
        self.shortcuts.append(QShortcut(QKeySequence("S"), self))
        self.shortcuts[-1].activated.connect(self.on_save_annotations)
        
        # Tab键 - 切换RGB/TI模态
        self.shortcuts.append(QShortcut(QKeySequence(Qt.Key_Tab), self))
        self.shortcuts[-1].activated.connect(self.toggleModality)
            
        # 1-9数字键 - 快速分配ID
        for i in range(1, 10):
            self.shortcuts.append(QShortcut(QKeySequence(str(i)), self))
            self.shortcuts[-1].activated.connect(lambda idx=i: self.quick_set_id(idx))
        
        # Q键 - 上一帧
        self.shortcuts.append(QShortcut(QKeySequence("Q"), self))
        self.shortcuts[-1].activated.connect(self.prev_frame)
        
        # W键 - 下一帧
        self.shortcuts.append(QShortcut(QKeySequence("W"), self))
        self.shortcuts[-1].activated.connect(self.next_frame)
        
        # E键 - 下一帧
        self.shortcuts.append(QShortcut(QKeySequence("E"), self))
        self.shortcuts[-1].activated.connect(self.next_frame)
        
        # 默认禁用所有快捷键
        self.disable_shortcuts()


    def select_annotation_by_id(self, color_id, modality):
        """通过ID和模态选择标注框"""
        # 首先查找对应的项目在树中的位置
        found_item = None
        
        for i in range(self.bbox_tree.topLevelItemCount()):
            item = self.bbox_tree.topLevelItem(i)
            ann_data = item.data(0, Qt.UserRole)
            
            if ann_data and ann_data.get("color_id") == color_id and ann_data.get("modality") == modality:
                found_item = item
                break
        
        if found_item:
            # 选中树中的项目，这将触发on_annotation_selected_in_tree
            self.bbox_tree.clearSelection()  # 先清除当前选择
            self.bbox_tree.setCurrentItem(found_item)
            self.log_message(f"已选中{modality}标注 ID{color_id}")
            
            # 如果当前不在相应模态的视图，切换到该模态
            if not self.two_column_mode and self.current_single_mode != modality:
                if modality == "RGB" and self.current_single_mode != "RGB":
                    self.toggleModality()
                elif modality == "TI" and self.current_single_mode != "TI":
                    self.toggleModality()
        else:
            self.log_message(f"无法找到{modality}标注 ID{color_id}")

    def show_shortcuts_help(self):
        """显示快捷键帮助对话框"""
        help_text = (
            "<h3>快捷键参考:</h3>"
            "<table>"
            "<tr><td>空格键</td><td>下一帧</td></tr>"  # 修改
            "<tr><td>P键</td><td>播放/暂停</td></tr>"  # 新增
            "<tr><td>←/→</td><td>上/下一帧</td></tr>"
            "<tr><td>1-9</td><td>快速选择对应ID</td></tr>"
            "<tr><td>D键</td><td>删除选中标注</td></tr>"
            "<tr><td>S键</td><td>保存标注</td></tr>"
            "<tr><td>Tab键</td><td>切换RGB/TI模态</td></tr>"
            "<tr><td>Q键</td><td>上一帧</td></tr>"
            "<tr><td>W/E键</td><td>下一帧</td></tr>"
            "<tr><td>方向键+Ctrl</td><td>移动标注</td></tr>"
            "<tr><td>方向键+Shift</td><td>调整标注大小</td></tr>"
            "</table>"
        )
        
        dialog = QDialog(self)
        dialog.setWindowTitle("快捷键帮助")
        layout = QVBoxLayout(dialog)
        
        label = QLabel(help_text)
        label.setTextFormat(Qt.RichText)
        layout.addWidget(label)
        
        close_btn = QPushButton("关闭")
        close_btn.clicked.connect(dialog.accept)
        layout.addWidget(close_btn)
        
        dialog.setFixedSize(300, 250)
        dialog.exec_()

    def quick_set_id(self, id_number):
        """根据数字快速选择相应ID的按钮"""
        # 查找对应ID的按钮并选择
        for btn in self.annotation_button_group.buttons():
            if self.annotation_button_group.id(btn) == id_number:
                # 选中对应的按钮
                btn.setChecked(True)
                
                # 获取颜色
                color = btn.styleSheet().split("color:")[1].split(";")[0].strip()
                
                # 更新当前视图的标注状态
                if self.two_column_mode:
                    # 双栏模式下，同时更新两个视图
                    self.rgb_view.set_annotation_enabled(True)
                    self.ti_view.set_annotation_enabled(True)
                    self.rgb_view.set_current_color(id_number, color)
                    self.ti_view.set_current_color(id_number, color)
                    self.log_message(f"已选择ID {id_number} (快捷键)")
                else:
                    # 单栏模式下，只更新当前模态的视图
                    if self.current_single_mode == "RGB":
                        self.rgb_view.set_annotation_enabled(True)
                        self.ti_view.set_annotation_enabled(False)
                        self.rgb_view.set_current_color(id_number, color)
                        self.log_message(f"已选择ID {id_number} (快捷键)")
                    else:
                        self.rgb_view.set_annotation_enabled(False)
                        self.ti_view.set_annotation_enabled(True)
                        self.ti_view.set_current_color(id_number, color)
                        self.log_message(f"已选择ID {id_number} (快捷键)")
                
                # 执行相同的逻辑，确保与UI点击选择按钮效果一致
                self.on_annotation_selected(btn)
                return
        
        self.log_message(f"ID {id_number} 不可用")

    def adjust_annotation_position(self, dx, dy):
        """调整当前选中标注的位置"""
        if not hasattr(self, 'highlighted_item') or not self.highlighted_item:
            self.log_message("未选中标注框")
            return
            
        # 获取调整步长
        step = self.adjust_step.value() if hasattr(self, 'adjust_step') else 1
        dx *= step
        dy *= step
        
        # 更新坐标输入框，这将通过绑定的槽函数触发位置更新
        self.edit_x.setValue(self.edit_x.value() + dx)
        self.edit_y.setValue(self.edit_y.value() + dy)
        
        # 立即应用修改
        self.apply_annotation_edit()

    def adjust_annotation_size(self, dw, dh):
        """调整当前选中标注的尺寸"""
        if not hasattr(self, 'highlighted_item') or not self.highlighted_item:
            self.log_message("未选中标注框")
            return
            
        # 获取调整步长
        step = self.adjust_step.value() if hasattr(self, 'adjust_step') else 1
        dw *= step
        dh *= step
        
        # 更新尺寸输入框，这将通过绑定的槽函数触发尺寸更新
        self.edit_w.setValue(self.edit_w.value() + dw)
        self.edit_h.setValue(self.edit_h.value() + dh)
    
        # 立即应用修改
        self.apply_annotation_edit()

    def update_annotation_position(self):
        """根据编辑框更新标注位置"""
        if not hasattr(self, 'highlighted_item') or not self.highlighted_item:
            return
            
        try:
            # 获取新的坐标
            x = self.edit_x.value()
            y = self.edit_y.value()
            
            # 检索突出显示项的尺寸
            rect = self.highlighted_item.rect()
            width = rect.width()
            height = rect.height()
            
            # 更新矩形位置
            self.highlighted_item.setRect(QRectF(x, y, width, height))
            
            # 更新预览 - 但不立即应用到数据
            self.log_message(f"更新位置: ({x}, {y})")
        except Exception as e:
            self.log_message(f"更新位置出错: {str(e)}")

    def update_annotation_size(self):
        """根据编辑框更新标注大小"""
        if not hasattr(self, 'highlighted_item') or not self.highlighted_item:
            return
            
        try:
            # 获取新的尺寸
            width = self.edit_w.value()
            height = self.edit_h.value()
            
            # 检索突出显示项的位置
            rect = self.highlighted_item.rect()
            x = rect.x()
            y = rect.y()
            
            # 更新矩形尺寸
            self.highlighted_item.setRect(QRectF(x, y, width, height))
            
            # 更新预览 - 但不立即应用到数据
            self.log_message(f"更新尺寸: ({width}×{height})")
        except Exception as e:
            self.log_message(f"更新尺寸出错: {str(e)}")

    def apply_annotation_edit(self):
        """应用所有编辑到当前选中的标注框"""
        if not hasattr(self, 'highlighted_item') or not self.highlighted_item:
            self.log_message("未选中标注框")
            return
            
        try:
            # 获取新的坐标和尺寸
            x = self.edit_x.value()
            y = self.edit_y.value()
            width = self.edit_w.value()
            height = self.edit_h.value()
            
            # 获取标注数据
            rect = QRectF(x, y, width, height)
            item_data = getattr(self.highlighted_item, 'annotation_data', {})
            color_id = item_data.get('color_id')
            modality = item_data.get('modality')
            
            if not color_id or not modality:
                self.log_message("标注数据不完整，无法更新")
                return
            
            # 查找当前标注在self.annotations中的索引
            current_ann = None
            for ann in self.annotations:
                if (ann.get("frame") == self.current_frame_index and
                    ann.get("color_id") == color_id and
                    ann.get("modality") == modality):
                    current_ann = ann
                    break
            
            if not current_ann:
                self.log_message("无法找到要更新的标注")
                return
                
            # 更新标注数据
            current_ann["bbox"] = [x, y, width, height]
            
            # 更新树视图
            self.update_bbox_tree()
            
            # 重绘所有标注以反映更改
            if modality == "RGB":
                self.rgb_view.clearColorAnnotation(color_id, modality="RGB")
                color = self.color_map.get(color_id, "#FFFFFF")
                self.rgb_view.drawAnnotation(rect, color, color_id, modality="RGB")
            else:
                self.ti_view.clearColorAnnotation(color_id, modality="TI")
                color = self.color_map.get(color_id, "#FFFFFF")
                self.ti_view.drawAnnotation(rect, color, color_id, modality="TI")
            
            # 标记已修改
            self.annotation_modified = True
            
            # 更新缓存
            if self.cached_video_id:
                frame_annotations = [ann for ann in self.annotations if ann.get("frame") == self.current_frame_index]
                self.annotation_cache[self.current_frame_index] = frame_annotations.copy()
                
            self.log_message(f"已应用标注框修改: ({x}, {y}, {width}×{height})")
        except Exception as e:
            self.log_message(f"应用修改出错: {str(e)}")

    def enable_edit_controls(self):
        """启用标注框编辑控件"""
        # 编辑框控件
        self.edit_x.setEnabled(True)
        self.edit_y.setEnabled(True)
        self.edit_w.setEnabled(True)
        self.edit_h.setEnabled(True)
        self.apply_edit_btn.setEnabled(True)
        
        # 微调按钮
        if hasattr(self, 'adjustment_layout') and self.adjustment_layout:
            for i in range(self.adjustment_layout.count()):
                widget = self.adjustment_layout.itemAt(i).widget()
                if isinstance(widget, QPushButton):
                    widget.setEnabled(True)

    def disable_edit_controls(self):
        """禁用标注框编辑控件"""
        # 编辑框控件
        self.edit_x.setEnabled(False)
        self.edit_y.setEnabled(False)
        self.edit_w.setEnabled(False)
        self.edit_h.setEnabled(False)
        self.apply_edit_btn.setEnabled(False)
        
        # 微调按钮
        if hasattr(self, 'adjustment_layout') and self.adjustment_layout:
            for i in range(self.adjustment_layout.count()):
                widget = self.adjustment_layout.itemAt(i).widget()
                if isinstance(widget, QPushButton):
                    widget.setEnabled(False)

    def keyPressEvent(self, event):
        """处理键盘事件"""
        if self.shortcuts_enabled:
            # 获取当前调整步长
            step = self.adjust_step.value() if hasattr(self, 'adjust_step') else 1
            # Alt键可以加速调整
            if event.modifiers() & Qt.AltModifier:
                step *= 5
            
            # 检查是否有选中的标注
            has_selected = hasattr(self, 'highlighted_item') and self.highlighted_item
            
            # 标注框调整快捷键
            if has_selected:
                # Shift+方向键：调整大小
                if event.modifiers() & Qt.ShiftModifier:
                    if event.key() == Qt.Key_Left:
                        self.adjust_annotation_size(-step, 0)
                        return
                    elif event.key() == Qt.Key_Right:
                        self.adjust_annotation_size(step, 0)
                        return
                    elif event.key() == Qt.Key_Up:
                        self.adjust_annotation_size(0, -step)
                        return
                    elif event.key() == Qt.Key_Down:
                        self.adjust_annotation_size(0, step)
                        return
                    
                # Ctrl+方向键：调整位置
                elif event.modifiers() & Qt.ControlModifier:
                    if event.key() == Qt.Key_Left:
                        self.adjust_annotation_position(-step, 0)
                        return
                    elif event.key() == Qt.Key_Right:
                        self.adjust_annotation_position(step, 0)
                        return
                    elif event.key() == Qt.Key_Up:
                        self.adjust_annotation_position(0, -step)
                        return
                    elif event.key() == Qt.Key_Down:
                        self.adjust_annotation_position(0, step)
                        return
            
            # 通用快捷键 - 不需要选中标注
            if event.key() == Qt.Key_Space:
                self.next_frame()  # 修改为下一帧
                return
            elif event.key() == Qt.Key_P:  # 新增P键处理
                self.toggle_play()
                return
            elif event.key() == Qt.Key_Left and not (event.modifiers() & (Qt.ShiftModifier | Qt.ControlModifier)):
                self.prev_frame()
                return
            elif event.key() == Qt.Key_Right and not (event.modifiers() & (Qt.ShiftModifier | Qt.ControlModifier)):
                self.next_frame()
                return
            elif event.key() == Qt.Key_D and has_selected:
                self.delete_selected_annotation()
                return
            elif event.key() == Qt.Key_S:
                self.on_save_annotations()
                return

    def transform_coordinates_between_modalities(self, rect, from_modality, to_modality):
        """在不同分辨率模态之间转换标注框坐标，应用用户定义的偏移量
        
        Args:
            rect: QRectF 要转换的矩形
            from_modality: 源模态 ("RGB" 或 "TI")
            to_modality: 目标模态 ("RGB" 或 "TI")
        
        Returns:
            转换后的 QRectF
        """
        if from_modality == to_modality:
            return QRectF(rect)  # 相同模态直接返回副本
            
        # 获取偏移值
        offset_x = self.offset_slider_x.value()
        offset_y = self.offset_slider_y.value()
            
        # 获取源图像和目标图像的尺寸
        source_size = None
        target_size = None
        
        if from_modality == "RGB" and hasattr(self, 'rgb') and self.rgb is not None:
            source_size = (self.rgb.shape[1], self.rgb.shape[0])  # (宽, 高)
        elif from_modality == "TI" and hasattr(self, 'ti') and self.ti is not None:
            source_size = (self.ti.shape[1], self.ti.shape[0])
        
        if to_modality == "RGB" and hasattr(self, 'rgb') and self.rgb is not None:
            target_size = (self.rgb.shape[1], self.rgb.shape[0])
        elif to_modality == "TI" and hasattr(self, 'ti') and self.ti is not None:
            target_size = (self.ti.shape[1], self.ti.shape[0])
        
        # 如果无法获取尺寸，返回原始矩形
        if not source_size or not target_size:
            self.log_message(f"警告: 无法获取{from_modality}或{to_modality}图像尺寸，使用原始坐标")
            return QRectF(rect)
        
        # 计算源图像和目标图像的中心点
        source_center_x = source_size[0] / 2
        source_center_y = source_size[1] / 2
        target_center_x = target_size[0] / 2
        target_center_y = target_size[1] / 2
        
        # 计算缩放比例
        scale_x = target_size[0] / source_size[0]
        scale_y = target_size[1] / source_size[1]
        
        # 计算源矩形的中心点相对于源图像中心的偏移
        rect_center_x = rect.x() + rect.width() / 2
        rect_center_y = rect.y() + rect.height() / 2
        
        rel_x = rect_center_x - source_center_x
        rel_y = rect_center_y - source_center_y
        
        # 将相对偏移应用到目标图像（加上用户定义的偏移）
        new_center_x = target_center_x + rel_x * scale_x + (offset_x if from_modality == "RGB" else -offset_x)
        new_center_y = target_center_y + rel_y * scale_y + (offset_y if from_modality == "RGB" else -offset_y)
        
        # 调整宽度和高度
        new_width = rect.width() * scale_x
        new_height = rect.height() * scale_y
        
        # 计算左上角坐标
        new_x = new_center_x - new_width / 2
        new_y = new_center_y - new_height / 2
        
        self.log_message(f"坐标转换(含偏移X:{offset_x},Y:{offset_y}): {from_modality}({int(rect.x())},{int(rect.y())}) → "
                        f"{to_modality}({int(new_x)},{int(new_y)})")
        
        return QRectF(new_x, new_y, new_width, new_height)

    def update_movement(self):
        """此方法保留但不再用于移动画面，只为兼容性保留"""
        # 不再实现任何画面移动功能
        pass

    
    def track_annotation(self, start_frame=None, end_frame=None):
        """跟踪当前选中的标注框在连续帧中的位置"""
        # 检查是否选中了标注
        selected_items = self.bbox_tree.selectedItems()
        if not selected_items:
            QMessageBox.warning(self, "警告", "请先选择一个要跟踪的标注框")
            return
        
        item = selected_items[0]
        ann = item.data(0, Qt.UserRole)
        if not ann:
            QMessageBox.warning(self, "警告", "无效的标注数据")
            return
        
        # 获取标注信息
        color_id = ann.get("color_id")
        modality = ann.get("modality")
        bbox = ann.get("bbox")  # [x, y, width, height]
        current_frame = self.current_frame_index
        
        # 设置跟踪范围
        if start_frame is None:
            start_frame = current_frame
        if end_frame is None:
            # 默认跟踪到视频末尾
            if modality == "RGB" and hasattr(self, 'rgb_frames'):
                end_frame = len(self.rgb_frames) - 1
            elif modality == "TI" and hasattr(self, 'ti_frames'):
                end_frame = len(self.ti_frames) - 1
            else:
                QMessageBox.warning(self, "警告", "无法确定视频帧数量")
                return
        
        # 创建进度对话框
        progress = QProgressDialog("正在跟踪目标...", "取消", start_frame, end_frame, self)
        progress.setWindowTitle("目标跟踪")
        progress.setWindowModality(Qt.WindowModal)
        progress.show()
        
        # 当前帧标注已经有了，从下一帧开始跟踪
        start_frame = current_frame + 1
        
        # 初始化跟踪器（使用 dlib 因为 OpenCV 的跟踪器 API 可能不兼容）
        try:
            import dlib
            tracker = dlib.correlation_tracker()
            using_dlib = True
            self.log_message("使用 dlib 跟踪器")
        except ImportError:
            # 如果没有 dlib，尝试使用 OpenCV 的跟踪器
            self.log_message("未找到 dlib，回退到 OpenCV 跟踪器")
            try:
                # 尝试 OpenCV 4.x API
                tracker = cv2.TrackerCSRT.create()
                using_dlib = False
            except AttributeError:
                try:
                    # 尝试 OpenCV 3.x API
                    tracker = cv2.TrackerCSRT_create()
                    using_dlib = False
                except AttributeError:
                    QMessageBox.warning(self, "错误", "无法创建跟踪器。请安装 dlib: pip install dlib")
                    progress.close()
                    return
        
        # 初始化跟踪器
        if modality == "RGB" and hasattr(self, 'rgb'):
            init_frame = self.rgb.copy()
            frame_list = self.rgb_frames
        elif modality == "TI" and hasattr(self, 'ti'):
            init_frame = self.ti.copy()
            frame_list = self.ti_frames
        else:
            QMessageBox.warning(self, "警告", f"无法获取{modality}图像")
            progress.close()
            return
        
        # 确保当前帧图像存在
        if init_frame is None:
            QMessageBox.warning(self, "警告", "当前帧图像无效，无法初始化跟踪器")
            progress.close()
            return
        
        # 初始化跟踪器 - 方式取决于使用的库
        x, y, w, h = [int(v) for v in bbox]
        success = False
        
        if using_dlib:
            # dlib 需要 rectangle 对象
            dlib_rect = dlib.rectangle(x, y, x+w, y+h)
            tracker.start_track(init_frame, dlib_rect)
            success = True  # dlib 没有返回初始化状态，假设成功
        else:
            # OpenCV 需要 (x,y,w,h) 元组
            success = tracker.init(init_frame, (x, y, w, h))
        
        if not success:
            QMessageBox.warning(self, "警告", "跟踪器初始化失败")
            progress.close()
            return
        
        # 计算颜色
        color = self.color_map.get(color_id, "#FFFFFF")
        
        # 跟踪后添加的标注数量
        tracked_count = 0
        
        # 保存操作前的当前帧索引
        original_frame_index = self.current_frame_index
        
        try:
            for frame_idx in range(start_frame, end_frame + 1):
                # 检查是否取消
                if progress.wasCanceled():
                    break
                    
                # 更新进度条
                progress.setValue(frame_idx)
                QApplication.processEvents()
                
                # 加载要跟踪的帧
                frame_path = None
                if frame_idx < len(frame_list):
                    frame_path = frame_list[frame_idx]
                
                if not frame_path or not os.path.exists(frame_path):
                    self.log_message(f"跳过帧 {frame_idx+1}，找不到图像文件")
                    continue
                    
                # 读取帧图像
                frame = cv2.imread(frame_path)
                if frame is None:
                    self.log_message(f"跳过帧 {frame_idx+1}，无法读取图像")
                    continue
                
                # 更新跟踪器
                if using_dlib:
                    # dlib 跟踪器更新
                    tracker.update(frame)
                    pos = tracker.get_position()
                    new_x = pos.left()
                    new_y = pos.top()
                    new_w = pos.width()
                    new_h = pos.height()
                    success = True
                else:
                    # OpenCV 跟踪器更新
                    success, box = tracker.update(frame)
                    if success:
                        new_x, new_y, new_w, new_h = box
                
                if success:
                    # 跟踪成功，创建新标注
                    new_ann = {
                        "frame": frame_idx,
                        "bbox": [float(new_x), float(new_y), float(new_w), float(new_h)],
                        "color_id": color_id,
                        "modality": modality
                    }
                    
                    # 检查是否已有相同ID和模态的标注
                    has_existing = False
                    for existing_ann in self.annotations:
                        if (existing_ann.get("frame") == frame_idx and 
                            existing_ann.get("color_id") == color_id and
                            existing_ann.get("modality") == modality):
                            # 更新已有标注
                            existing_ann["bbox"] = [float(new_x), float(new_y), float(new_w), float(new_h)]
                            has_existing = True
                            break

                    if not has_existing:
                        self.annotations.append(new_ann)
                        tracked_count += 1
                    
                    # 更新缓存
                    if self.cached_video_id:
                        if frame_idx not in self.annotation_cache:
                            self.annotation_cache[frame_idx] = []
                        
                        # 检查缓存中是否已有该标注
                        cache_has_existing = False
                        for i, cache_ann in enumerate(self.annotation_cache[frame_idx]):
                            if (cache_ann.get("color_id") == color_id and
                                cache_ann.get("modality") == modality):
                                self.annotation_cache[frame_idx][i]["bbox"] = [
                                    float(new_x), float(new_y), float(new_w), float(new_h)
                                ]
                                cache_has_existing = True
                                break
                        
                        if not cache_has_existing:
                            self.annotation_cache[frame_idx].append(new_ann.copy())
                else:
                    self.log_message(f"在帧 {frame_idx+1} 中跟踪失败，目标可能消失")
                    # 可以选择继续跟踪下一帧或中断
                    # break  # 如果要在目标丢失时停止跟踪，取消注释此行
        
        except Exception as e:
            import traceback
            self.log_message(f"跟踪过程中发生错误: {str(e)}")
            traceback.print_exc()
            QMessageBox.critical(self, "错误", f"跟踪过程中发生错误: {str(e)}")
        finally:
            progress.close()
        
        # 标记标注已修改
        self.annotation_modified = True
        
        # 恢复到原始帧
        self.current_frame_index = original_frame_index
        self.frame_slider.setValue(original_frame_index)
        
        # 保存标注到磁盘
        self.save_all_annotations_to_disk()
        
        # 更新当前帧的标注树和显示
        self.update_bbox_tree()
        self.redraw_all_annotations()
        
        # 显示结果
        QMessageBox.information(self, "跟踪完成", f"已在{tracked_count}个帧中跟踪并添加标注")
        self.log_message(f"目标跟踪完成: ID{color_id}在{modality}模态下跟踪了{tracked_count}帧")
    
    def track_annotation_with_dlib(self, start_frame=None, end_frame=None):
        """使用dlib进行对象跟踪"""
        try:
            import dlib
        except ImportError:
            QMessageBox.warning(self, "错误", "未找到dlib库，请安装: pip install dlib")
            return
        
        # 检查是否选中了标注
        selected_items = self.bbox_tree.selectedItems()
        if not selected_items:
            QMessageBox.warning(self, "警告", "请先选择一个要跟踪的标注框")
            return
        
        item = selected_items[0]
        ann = item.data(0, Qt.UserRole)
        if not ann:
            QMessageBox.warning(self, "警告", "无效的标注数据")
            return
        
        # 获取标注信息
        color_id = ann.get("color_id")
        modality = ann.get("modality")
        bbox = ann.get("bbox")  # [x, y, width, height]
        current_frame = self.current_frame_index
        
        # 设置跟踪范围
        if start_frame is None:
            start_frame = current_frame
        if end_frame is None:
            if modality == "RGB" and hasattr(self, 'rgb_frames'):
                end_frame = len(self.rgb_frames) - 1
            elif modality == "TI" and hasattr(self, 'ti_frames'):
                end_frame = len(self.ti_frames) - 1
            else:
                QMessageBox.warning(self, "警告", "无法确定视频帧数量")
                return
        
        # 创建进度对话框
        progress = QProgressDialog("正在跟踪目标...", "取消", start_frame, end_frame, self)
        progress.setWindowTitle("目标跟踪")
        progress.setWindowModality(Qt.WindowModal)
        progress.show()
        
        # 当前帧标注已经有了，从下一帧开始跟踪
        start_frame = current_frame + 1
        
        # 创建dlib跟踪器
        tracker = dlib.correlation_tracker()
        
        # 初始化跟踪器
        if modality == "RGB" and hasattr(self, 'rgb'):
            init_frame = self.rgb
            frame_list = self.rgb_frames
        elif modality == "TI" and hasattr(self, 'ti'):
            init_frame = self.ti
            frame_list = self.ti_frames
        else:
            QMessageBox.warning(self, "警告", f"无法获取{modality}图像")
            progress.close()
            return
        
        # 初始化dlib跟踪器 (需要将[x,y,w,h]转换为dlib矩形[left,top,right,bottom])
        x, y, w, h = bbox
        dlib_rect = dlib.rectangle(int(x), int(y), int(x+w), int(y+h))
        tracker.start_track(init_frame, dlib_rect)
        
        # 计算颜色
        color = self.color_map.get(color_id, "#FFFFFF")
        
        # 跟踪后添加的标注数量
        tracked_count = 0
        
        # 保存操作前的当前帧索引
        original_frame_index = self.current_frame_index
        
        try:
            # 跟踪所有帧
            for frame_idx in range(start_frame, end_frame + 1):
                # 检查是否取消
                if progress.wasCanceled():
                    break
                    
                # 更新进度条
                progress.setValue(frame_idx)
                QApplication.processEvents()
                
                # 加载要跟踪的帧
                frame_path = None
                if frame_idx < len(frame_list):
                    frame_path = frame_list[frame_idx]
                
                if not frame_path or not os.path.exists(frame_path):
                    self.log_message(f"跳过帧 {frame_idx+1}，找不到图像文件")
                    continue
                    
                # 读取帧图像
                frame = cv2.imread(frame_path)
                if frame is None:
                    self.log_message(f"跳过帧 {frame_idx+1}，无法读取图像")
                    continue
                
                # 更新跟踪器
                tracker.update(frame)
                position = tracker.get_position()
                
                # 提取结果
                x = position.left()
                y = position.top()
                w = position.width()
                h = position.height()
                
                # 创建新标注
                new_ann = {
                    "frame": frame_idx,
                    "bbox": [x, y, w, h],
                    "color_id": color_id,
                    "modality": modality
                }
                
                # 检查是否已有相同ID和模态的标注
                has_existing = False
                for existing_ann in self.annotations:
                    if (existing_ann.get("frame") == frame_idx and 
                        existing_ann.get("color_id") == color_id and
                        existing_ann.get("modality") == modality):
                        # 更新已有标注
                        existing_ann["bbox"] = [x, y, w, h]
                        has_existing = True
                        break
                
                if not has_existing:
                    self.annotations.append(new_ann)
                    tracked_count += 1
                
                # 更新缓存
                if self.cached_video_id:
                    if frame_idx not in self.annotation_cache:
                        self.annotation_cache[frame_idx] = []
                    
                    # 检查缓存中是否已有该标注
                    cache_has_existing = False
                    for i, cache_ann in enumerate(self.annotation_cache[frame_idx]):
                        if (cache_ann.get("color_id") == color_id and
                            cache_ann.get("modality") == modality):
                            self.annotation_cache[frame_idx][i]["bbox"] = [x, y, w, h]
                            cache_has_existing = True
                            break
                    
                    if not cache_has_existing:
                        self.annotation_cache[frame_idx].append(new_ann.copy())
        
        except Exception as e:
            QMessageBox.critical(self, "错误", f"跟踪过程中发生错误: {str(e)}")
        finally:
            progress.close()
        
        # 标记标注已修改
        self.annotation_modified = True
        
        # 恢复到原始帧
        self.current_frame_index = original_frame_index
        self.frame_slider.setValue(original_frame_index)
        
        # 保存标注到磁盘
        self.save_all_annotations_to_disk()
        
        # 更新当前帧的标注树和显示
        self.update_bbox_tree()
        self.redraw_all_annotations()
        
        # 显示结果
        QMessageBox.information(self, "跟踪完成", f"已在{tracked_count}个帧中跟踪并添加标注")
        self.log_message(f"目标跟踪完成: ID{color_id}在{modality}模态下跟踪了{tracked_count}帧")

    def show_track_dialog(self):
        """显示跟踪参数设置对话框"""
        # 检查是否选中了标注
        selected_items = self.bbox_tree.selectedItems()
        if not selected_items:
            QMessageBox.warning(self, "警告", "请先选择一个要跟踪的标注框")
            return
        
        # 获取视频总帧数
        max_frames = 0
        if hasattr(self, 'rgb_frames') and self.rgb_frames:
            max_frames = max(max_frames, len(self.rgb_frames))
        if hasattr(self, 'ti_frames') and self.ti_frames:
            max_frames = max(max_frames, len(self.ti_frames))
        
        if max_frames == 0:
            QMessageBox.warning(self, "警告", "无法获取视频帧数")
            return
        
        # 创建对话框
        dialog = QDialog(self)
        dialog.setWindowTitle("目标跟踪设置")
        layout = QVBoxLayout(dialog)
        
        # 添加范围选择
        form = QFormLayout()
        
        # 当前帧
        current_frame_label = QLabel(f"当前帧: {self.current_frame_index + 1}")
        form.addRow(current_frame_label)
        
        # 开始帧
        start_frame = QSpinBox()
        start_frame.setRange(self.current_frame_index + 1, max_frames)
        start_frame.setValue(self.current_frame_index + 1)
        start_frame.setToolTip("从哪一帧开始跟踪 (当前帧已有标注)")
        form.addRow("开始帧:", start_frame)
        
        # 结束帧
        end_frame = QSpinBox()
        end_frame.setRange(self.current_frame_index + 1, max_frames)
        end_frame.setValue(max_frames)
        end_frame.setToolTip("跟踪到哪一帧结束")
        form.addRow("结束帧:", end_frame)
        
        # 跟踪算法选择
        tracker_combo = QComboBox()
        tracker_combo.addItems([
            "轻量级跟踪 (低性能需求)",
            "DeepSORT (适合夜间场景)",
            "Dlib (稳定通用)"
        ])
        form.addRow("跟踪算法:", tracker_combo)
        
        # 同步到另一模态
        sync_checkbox = QCheckBox("将跟踪结果自动同步到另一模态")
        sync_checkbox.setChecked(True)
        form.addRow(sync_checkbox)
        
        layout.addLayout(form)
        
        # 添加按钮
        btn_layout = QHBoxLayout()
        ok_btn = QPushButton("开始跟踪")
        cancel_btn = QPushButton("取消")
        btn_layout.addWidget(ok_btn)
        btn_layout.addWidget(cancel_btn)
        layout.addLayout(btn_layout)
        
        # 连接信号
        ok_btn.clicked.connect(dialog.accept)
        cancel_btn.clicked.connect(dialog.reject)
        
        # 显示对话框
        if dialog.exec_() == QDialog.Accepted:
            # 获取选择的参数
            start_idx = start_frame.value() - 1
            end_idx = end_frame.value() - 1
            sync_to_other = sync_checkbox.isChecked()
            
            # 根据选择的算法调用不同的跟踪函数
            tracker_type = tracker_combo.currentIndex()
            if tracker_type == 1:  # DeepSORT
                self.track_annotation_with_deep_sort(start_idx, end_idx)
            elif tracker_type == 2:  # Dlib
                self.track_annotation_with_dlib(start_idx, end_idx)
            else:  # 默认或轻量级跟踪
                self.track_annotation(start_idx, end_idx)
                
            # 如果需要同步到另一模态
            if sync_to_other:
                self.sync_tracking_to_other_modality(start_idx, end_idx)
                
                
    def sync_tracking_to_other_modality(self, start_frame, end_frame):
        """将跟踪结果同步到另一个模态"""
        # 获取选中的标注
        selected_items = self.bbox_tree.selectedItems()
        if not selected_items:
            return
            
        item = selected_items[0]
        ann = item.data(0, Qt.UserRole)
        if not ann:
            return
            
        # 获取标注信息
        color_id = ann.get("color_id")
        source_modality = ann.get("modality")
        target_modality = "TI" if source_modality == "RGB" else "RGB"
        
        # 显示进度对话框
        progress = QProgressDialog(f"正在同步到{target_modality}模态...", "取消", start_frame, end_frame, self)
        progress.setWindowTitle("同步跟踪结果")
        progress.setWindowModality(Qt.WindowModal)
        progress.show()
        
        synced_count = 0
        
        try:
            # 遍历指定范围内的每一帧
            for frame_idx in range(start_frame, end_frame + 1):
                # 检查是否取消
                if progress.wasCanceled():
                    break
                    
                # 更新进度条
                progress.setValue(frame_idx)
                QApplication.processEvents()
                
                # 查找源模态的标注
                source_ann = None
                for a in self.annotations:
                    if (a.get("frame") == frame_idx and 
                        a.get("color_id") == color_id and 
                        a.get("modality") == source_modality):
                        source_ann = a
                        break
                        
                if not source_ann:
                    continue  # 如果该帧没有源模态的标注，跳过
                    
                # 创建矩形并转换坐标
                source_rect = QRectF(*source_ann.get("bbox"))
                target_rect = self.transform_coordinates_between_modalities(
                    source_rect, source_modality, target_modality)
                    
                # 创建目标模态的标注
                target_ann = {
                    "frame": frame_idx,
                    "bbox": [target_rect.x(), target_rect.y(), target_rect.width(), target_rect.height()],
                    "color_id": color_id,
                    "modality": target_modality
                }
                
                # 检查是否已有相同的标注
                existing_target = None
                for a in self.annotations:
                    if (a.get("frame") == frame_idx and 
                        a.get("color_id") == color_id and 
                        a.get("modality") == target_modality):
                        existing_target = a
                        break
                        
                if existing_target:
                    # 更新已有标注
                    existing_target["bbox"] = target_ann["bbox"]
                else:
                    # 添加新标注
                    self.annotations.append(target_ann)
                    synced_count += 1
                    
                # 更新缓存
                if self.cached_video_id:
                    if frame_idx not in self.annotation_cache:
                        self.annotation_cache[frame_idx] = []
                    
                    # 检查缓存中是否已有该标注
                    cache_has_existing = False
                    for i, cache_ann in enumerate(self.annotation_cache[frame_idx]):
                        if (cache_ann.get("color_id") == color_id and
                            cache_ann.get("modality") == target_modality):
                            self.annotation_cache[frame_idx][i]["bbox"] = target_ann["bbox"]
                            cache_has_existing = True
                            break
                            
                    if not cache_has_existing:
                        self.annotation_cache[frame_idx].append(target_ann.copy())
        
        except Exception as e:
            QMessageBox.critical(self, "错误", f"同步跟踪结果时发生错误: {str(e)}")
        
        finally:
            progress.close()
            
        # 标记标注已修改
        self.annotation_modified = True
        
        # 保存标注到磁盘
        self.save_all_annotations_to_disk()
        
        # 刷新当前帧显示
        self.update_bbox_tree()
        self.redraw_all_annotations()
        
        # 显示结果
        self.log_message(f"已将ID{color_id}的跟踪结果同步到{target_modality}模态，共{synced_count}帧")
        
    def track_annotation_with_deep_sort(self, start_frame=None, end_frame=None):
        """使用DeepSORT进行目标跟踪 - 适合夜间和运动模糊场景"""
        try:
            # 只导入deep_sort_realtime库
            from deep_sort_realtime.deepsort_tracker import DeepSort
        except ImportError:
            QMessageBox.warning(self, "错误", 
                "未找到DeepSORT库，请安装: pip install deep-sort-realtime")
            return
            
        # 检查是否选中了标注
        selected_items = self.bbox_tree.selectedItems()
        if not selected_items:
            QMessageBox.warning(self, "警告", "请先选择一个要跟踪的标注框")
            return
    
        # 获取标注信息
        item = selected_items[0]
        ann = item.data(0, Qt.UserRole)
        if not ann:
            QMessageBox.warning(self, "警告", "无效的标注数据")
            return
    
        color_id = ann.get("color_id")
        modality = ann.get("modality")
        bbox = ann.get("bbox")  # [x, y, width, height]
        current_frame = self.current_frame_index
    
        # 设置跟踪范围
        if start_frame is None:
            start_frame = current_frame
        if end_frame is None:
            # 默认跟踪到视频末尾
            if modality == "RGB" and hasattr(self, 'rgb_frames'):
                end_frame = len(self.rgb_frames) - 1
            elif modality == "TI" and hasattr(self, 'ti_frames'):
                end_frame = len(self.ti_frames) - 1
            else:
                QMessageBox.warning(self, "警告", "无法确定视频帧数量")
                return
    
        # 创建进度对话框
        progress = QProgressDialog("正在跟踪目标...", "取消", start_frame, end_frame, self)
        progress.setWindowTitle("DeepSORT目标跟踪")
        progress.setWindowModality(Qt.WindowModal)
        progress.show()
    
        # 当前帧标注已经有了，从下一帧开始跟踪
        start_frame = current_frame + 1
    
        try:
            # 使用deep_sort_realtime库初始化跟踪器
            tracker = DeepSort(
                max_age=70,                  # 跟踪器在删除轨迹前最大等待帧数
                n_init=3,                    # 确认轨迹所需的连续检测次数
                nms_max_overlap=1.0,         # 非最大抑制阈值 
                max_cosine_distance=0.3,     # 余弦距离匹配阈值 (较小值更严格)
                nn_budget=150,               # 每个轨迹保持的特征数
                override_track_class=None,   # 默认跟踪类
                embedder="mobilenet",        # 使用MobileNet作为特征提取器
                half=True,                   # 使用半精度推理提高速度
                bgr=True,                    # 输入图像是BGR格式(OpenCV默认)
                embedder_gpu=True,           # 在GPU上运行特征提取
                embedder_model_name=None,    # 使用默认模型
                embedder_wts=None,           # 使用默认权重
                polygon=False,               # 不使用多边形框
                today=None                   # 自动生成时间戳
            )
            self.log_message("已成功初始化DeepSORT跟踪器(deep_sort_realtime)")
        except Exception as e:
            QMessageBox.critical(self, "错误", f"初始化DeepSORT跟踪器失败: {str(e)}")
            traceback.print_exc()
            progress.close()
            return
    
        # 初始帧和跟踪列表
        if modality == "RGB" and hasattr(self, 'rgb_frames'):
            frame_list = self.rgb_frames
            init_frame = self.rgb
        elif modality == "TI" and hasattr(self, 'ti_frames'):
            frame_list = self.ti_frames
            init_frame = self.ti
        else:
            QMessageBox.warning(self, "警告", f"无法获取{modality}图像")
            progress.close()
            return
    
        # 将初始边界框转换为跟踪器需要的格式
        x, y, w, h = [float(v) for v in bbox]
    
        # 记录跟踪的帧数和最后一个有效的检测框
        tracked_count = 0
        original_frame_index = self.current_frame_index
        last_valid_bbox = [x, y, w, h]  # 记录最后一个有效检测框
    
        # 从第一帧开始初始化
        first_frame_path = frame_list[current_frame]
        first_frame = cv2.imread(first_frame_path)
        track_id_mapping = {}  # 存储track_id到color_id的映射
    
        # 预处理图像增强 - 特别适用于夜间场景
        def preprocess_image(img):
            """预处理图像，确保格式正确并增强对比度"""
            if img is None:
                return None
            
            try:
                # 记录原始图像形状
                original_shape = img.shape
                
                # 确保图像为三通道BGR格式
                if len(img.shape) == 2:  # 灰度图像转BGR
                    img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
                elif len(img.shape) == 3:
                    if img.shape[2] == 1:  # 单通道三维图像转BGR
                        # 修正: 正确处理单通道图像
                        img = img.reshape(img.shape[0], img.shape[1]) 
                        img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
                    elif img.shape[2] == 4:  # RGBA格式转BGR
                        img = cv2.cvtColor(img, cv2.COLOR_RGBA2BGR)
                    # 检查图像形状是否不正常(如果通道数不是3)
                    elif img.shape[2] != 3:
                        # 尝试转换为灰度图再到BGR
                        try:
                            gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
                            img = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)
                        except Exception:
                            return None
                else:
                    # 不支持的图像维度
                    return None
                
                # 最终安全检查
                if len(img.shape) != 3 or img.shape[2] != 3:
                    return None
                
                # 应用CLAHE增强对比度
                img_yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV)
                clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
                img_yuv[:,:,0] = clahe.apply(img_yuv[:,:,0])
                img = cv2.cvtColor(img_yuv, cv2.COLOR_YUV2BGR)
                return img
            except Exception as e:
                # 处理异常
                return None
        
        try:
            # 初始化跟踪器 - 使用deep_sort_realtime API
            enhanced_frame = preprocess_image(first_frame)
            if enhanced_frame is None:
                raise ValueError("初始化图像预处理失败")
        
            # 确保bbox格式正确，使用更明确的初始化
            x1, y1 = int(x), int(y)
            x2, y2 = int(x + w), int(y + h)
        
            # 确保初始框可以被正确跟踪 - 使用正确的格式: [(bbox, confidence, class_name)]
            raw_detections = [([x1, y1, x2, y2], 1.0, 'person')]
            tracks = tracker.update_tracks(raw_detections, enhanced_frame)
        
            # 输出初始跟踪结果，便于调试
            self.log_message(f"初始化时的tracks数量: {len(tracks)}")
            for track in tracks:
                self.log_message(f"  Track {track.track_id}, 状态: {'已确认' if track.is_confirmed() else '未确认'}")
        
            # 强制创建映射关系，即使没有确认的跟踪目标
            for track in tracks:
                if track.is_confirmed():
                    track_id = track.track_id
                    track_id_mapping[track_id] = color_id
                    self.log_message(f"DeepSORT初始化成功，目标ID: {track_id}")
        
            # 如果没有确认的跟踪目标，强制使用第一个跟踪目标
            if not track_id_mapping and tracks:
                track_id = tracks[0].track_id
                track_id_mapping[track_id] = color_id
                self.log_message(f"强制设置跟踪ID映射: {track_id}")
        
            # 如果仍然没有映射，创建一个默认映射
            if not track_id_mapping:
                self.log_message("警告: 初始化跟踪时未能确认任何轨迹，使用默认ID")
                track_id_mapping[1] = color_id  # 使用ID 1作为默认跟踪ID
            
            # 逐帧跟踪
            for frame_idx in range(start_frame, end_frame + 1):
                # 检查是否取消
                if progress.wasCanceled():
                    break
                
                # 更新进度条
                progress.setValue(frame_idx)
                progress.setLabelText(f"正在跟踪帧 {frame_idx+1}/{end_frame+1}...")
                QApplication.processEvents()
            
                # 加载要跟踪的帧
                if frame_idx < len(frame_list):
                    frame_path = frame_list[frame_idx]
                else:
                    continue
                
                if not os.path.exists(frame_path):
                    self.log_message(f"跳过帧 {frame_idx+1}，找不到图像文件")
                    continue
                
                # 读取帧图像
                frame = cv2.imread(frame_path)
                if frame is None:
                    self.log_message(f"跳过帧 {frame_idx+1}，无法读取图像")
                    continue
            
                # 增强图像，改善跟踪效果
                enhanced_frame = preprocess_image(frame)
                if enhanced_frame is None:
                    self.log_message(f"跳过帧 {frame_idx+1}，图像预处理失败")
                    continue
            
                # 安全检查 - 确保图像格式正确
                if len(enhanced_frame.shape) != 3 or enhanced_frame.shape[2] != 3:
                    self.log_message(f"帧 {frame_idx+1} 图像格式不正确: {enhanced_frame.shape}")
                    continue
            
                # 创建一个从上一帧的位置估计的检测，帮助跟踪器保持跟踪
                try:
                    x_prev, y_prev, w_prev, h_prev = last_valid_bbox
                    x1 = int(x_prev)
                    y1 = int(y_prev)
                    x2 = int(x_prev + w_prev)
                    y2 = int(y_prev + h_prev)
                    
                    # 为跟踪器提供一个基于上一帧位置的推测检测 - 确保格式正确
                    detection_hint = [([x1, y1, x2, y2], 0.9, 'person')]
                    tracks = tracker.update_tracks(detection_hint, enhanced_frame)
                except Exception as e:
                    self.log_message(f"更新跟踪器出错: {str(e)}")
                    continue
            
                # 为跟踪器提供一个基于上一帧位置的推测检测
                detection_hint = [([x1, y1, x2, y2], 0.9, 'person')]
                tracks = tracker.update_tracks(detection_hint, enhanced_frame)
            
                # 记录这一帧的跟踪状态
                if frame_idx % 20 == 0:  # 每20帧输出一次日志，避免过多输出
                    self.log_message(f"帧 {frame_idx+1}: 共 {len(tracks)} 个跟踪目标, "
                                    f"{sum(1 for t in tracks if t.is_confirmed())} 个已确认")
            
                # 处理跟踪结果 - 使用放宽的条件接受跟踪目标
                found_target = False
                for track in tracks:
                    # 接受任何确认的轨迹，即使ID不在映射中
                    if track.is_confirmed():
                        # 如果ID不在映射中，添加它
                        if track.track_id not in track_id_mapping:
                            track_id_mapping[track.track_id] = color_id
                            self.log_message(f"添加新的跟踪ID映射: {track.track_id} -> {color_id}")
                    
                        # 获取跟踪框坐标(左上右下)
                        ltrb = track.to_ltrb()
                        x1, y1, x2, y2 = ltrb
                    
                        # 转换为[x,y,w,h]格式
                        new_x, new_y = x1, y1
                        new_w, new_h = x2-x1, y2-y1
                    
                        # 更新最后有效的检测框
                        last_valid_bbox = [float(new_x), float(new_y), float(new_w), float(new_h)]
                    
                        # 创建新标注
                        new_ann = {
                            "frame": frame_idx,
                            "bbox": last_valid_bbox,
                            "color_id": color_id,
                            "modality": modality
                        }
                    
                        # 检查是否已有相同ID和模态的标注
                        has_existing = False
                        for existing_ann in self.annotations:
                            if (existing_ann.get("frame") == frame_idx and 
                                existing_ann.get("color_id") == color_id and
                                existing_ann.get("modality") == modality):
                                # 更新已有标注
                                existing_ann["bbox"] = last_valid_bbox
                                has_existing = True
                                break
                    
                        if not has_existing:
                            self.annotations.append(new_ann)
                            tracked_count += 1
                    
                        # 更新缓存
                        if self.cached_video_id:
                            if frame_idx not in self.annotation_cache:
                                self.annotation_cache[frame_idx] = []
                        
                            # 检查缓存中是否已有该标注
                            cache_has_existing = False
                            for i, cache_ann in enumerate(self.annotation_cache[frame_idx]):
                                if (cache_ann.get("color_id") == color_id and
                                    cache_ann.get("modality") == modality):
                                    self.annotation_cache[frame_idx][i]["bbox"] = last_valid_bbox
                                    cache_has_existing = True
                                    break
                        
                            if not cache_has_existing:
                                self.annotation_cache[frame_idx].append(new_ann.copy())
                    
                        found_target = True
                        break  # 找到一个合适的跟踪目标后就停止
            
                # 如果当前帧没有找到跟踪目标，可以选择使用上一帧的位置
                if not found_target:
                    self.log_message(f"在帧 {frame_idx+1} 中目标丢失，使用估计位置")
                
                    # 创建基于上一帧位置的标注
                    new_ann = {
                        "frame": frame_idx,
                        "bbox": last_valid_bbox,
                        "color_id": color_id,
                        "modality": modality
                    }
                
                    # 检查是否已有相同ID和模态的标注
                    has_existing = False
                    for existing_ann in self.annotations:
                        if (existing_ann.get("frame") == frame_idx and 
                            existing_ann.get("color_id") == color_id and
                            existing_ann.get("modality") == modality):
                            # 更新已有标注
                            existing_ann["bbox"] = last_valid_bbox
                            has_existing = True
                            break
                
                    if not has_existing:
                        self.annotations.append(new_ann)
                        tracked_count += 1
                
                    # 更新缓存
                    if self.cached_video_id:
                        if frame_idx not in self.annotation_cache:
                            self.annotation_cache[frame_idx] = []
                    
                        # 检查缓存中是否已有该标注
                        cache_has_existing = False
                        for i, cache_ann in enumerate(self.annotation_cache[frame_idx]):
                            if (cache_ann.get("color_id") == color_id and
                                cache_ann.get("modality") == modality):
                                self.annotation_cache[frame_idx][i]["bbox"] = last_valid_bbox
                                cache_has_existing = True
                                break
                    
                        if not cache_has_existing:
                            self.annotation_cache[frame_idx].append(new_ann.copy())
    
        except Exception as e:
            traceback.print_exc()
            QMessageBox.critical(self, "错误", f"跟踪过程中发生错误: {str(e)}")
        finally:
            progress.close()
    
        # 收尾工作
        self.annotation_modified = True
        self.current_frame_index = original_frame_index
        self.frame_slider.setValue(original_frame_index)
        self.save_all_annotations_to_disk()
        self.update_bbox_tree()
        self.redraw_all_annotations()
    
        if tracked_count > 0:
            QMessageBox.information(self, "跟踪完成", f"已在{tracked_count}个帧中跟踪并添加标注")
            self.log_message(f"跟踪完成: 已生成{tracked_count}个标注")
        else:
            QMessageBox.warning(self, "跟踪完成", "未能成功跟踪目标")
            
    def show_delete_annotation_by_color_dialog(self):
        """显示删除指定颜色ID标注的对话框"""
        # 收集当前项目中使用的所有颜色ID
        used_color_ids = set()
        for ann in self.annotations:
            used_color_ids.add(ann.get("color_id"))
        
        # 如果使用了缓存，也检查缓存中的标注
        if hasattr(self, 'annotation_cache'):
            for frame_idx, anns in self.annotation_cache.items():
                for ann in anns:
                    used_color_ids.add(ann.get("color_id"))
        
        if not used_color_ids:
            QMessageBox.information(self, "提示", "当前项目没有任何标注")
            return
        
        # 创建对话框
        dialog = QDialog(self)
        dialog.setWindowTitle("按ID删除标注")
        layout = QVBoxLayout(dialog)
        
        layout.addWidget(QLabel("请选择要删除的标注ID:"))
        
        # 创建按钮组
        button_group = QButtonGroup(dialog)
        button_group.setExclusive(True)
        
        # 添加颜色选项
        for color_id in sorted(used_color_ids):
            radio = QRadioButton(f"ID {color_id} - {self._get_color_name(color_id)}")
            radio.setStyleSheet(f"color: {self.color_map.get(color_id, '#888888')}")
            button_group.addButton(radio, color_id)
            layout.addWidget(radio)
        
        # 添加删除选项
        delete_all_frames = QCheckBox("删除所有帧中的匹配标注")
        delete_all_frames.setChecked(True)
        layout.addWidget(delete_all_frames)
        
        # 添加操作按钮
        btn_layout = QHBoxLayout()
        delete_btn = QPushButton("删除")
        cancel_btn = QPushButton("取消")
        btn_layout.addWidget(delete_btn)
        btn_layout.addWidget(cancel_btn)
        layout.addLayout(btn_layout)
        
        # 连接信号
        delete_btn.clicked.connect(dialog.accept)
        cancel_btn.clicked.connect(dialog.reject)
        
        # 显示对话框
        if dialog.exec_() == QDialog.Accepted:
            selected_id = button_group.checkedId()
            if selected_id != -1:
                # 执行删除操作
                self.delete_annotation_by_color_id(selected_id, delete_all_frames.isChecked())
            else:
                QMessageBox.warning(self, "警告", "请先选择一个ID")

    def delete_annotation_by_color_id(self, color_id, all_frames=True):
        """删除指定颜色ID的所有标注
        
        Args:
            color_id: 要删除的颜色ID
            all_frames: 是否删除所有帧中的匹配标注，False则只删除当前帧
        """
        if all_frames:
            # 显示确认对话框
            reply = QMessageBox.question(
                self,
                "确认删除",
                f"确定要删除所有帧中ID为 {color_id} 的标注吗？这个操作无法撤销。",
                QMessageBox.Yes | QMessageBox.No,
                QMessageBox.No
            )
            
            if reply != QMessageBox.Yes:
                return
            
            # 计数删除的标注数量
            deleted_count = 0
            
            # 从内存中删除标注
            before_count = len(self.annotations)
            self.annotations = [ann for ann in self.annotations if ann.get("color_id") != color_id]
            deleted_count = before_count - len(self.annotations)
            
            # 更新缓存
            if hasattr(self, 'annotation_cache'):
                for frame_idx in list(self.annotation_cache.keys()):
                    before_count = len(self.annotation_cache[frame_idx])
                    self.annotation_cache[frame_idx] = [
                        ann for ann in self.annotation_cache[frame_idx] 
                        if ann.get("color_id") != color_id
                    ]
                    deleted_count += before_count - len(self.annotation_cache[frame_idx])
            
            # 保存更改到磁盘
            self.save_all_annotations_to_disk()
            
            # 更新当前视图
            self.rgb_view.clearColorAnnotation(color_id)
            self.ti_view.clearColorAnnotation(color_id)
            self.update_bbox_tree()
            self.redraw_all_annotations()
            
            # 显示结果
            self.log_message(f"已删除所有帧中ID为 {color_id} 的标注，共 {deleted_count} 项")
            
        else:
            # 只删除当前帧的标注
            reply = QMessageBox.question(
                self,
                "确认删除",
                f"确定要删除当前帧中ID为 {color_id} 的标注吗？",
                QMessageBox.Yes | QMessageBox.No,
                QMessageBox.No
            )
            
            if reply != QMessageBox.Yes:
                return
            
            # 计数删除的标注数量
            deleted_count = 0
            
            # 从内存中删除当前帧的标注
            before_count = len(self.annotations)
            self.annotations = [
                ann for ann in self.annotations 
                if not (ann.get("frame") == self.current_frame_index and ann.get("color_id") == color_id)
            ]
            deleted_count = before_count - len(self.annotations)
            
            # 更新缓存
            if hasattr(self, 'annotation_cache') and self.current_frame_index in self.annotation_cache:
                before_count = len(self.annotation_cache[self.current_frame_index])
                self.annotation_cache[self.current_frame_index] = [
                    ann for ann in self.annotation_cache[self.current_frame_index] 
                    if ann.get("color_id") != color_id
                ]
                deleted_count = before_count - len(self.annotation_cache[self.current_frame_index])
            
            # 保存更改到磁盘
            self.save_annotations_to_disk()
            
            # 更新当前视图
            self.rgb_view.clearColorAnnotation(color_id)
            self.ti_view.clearColorAnnotation(color_id)
            self.update_bbox_tree()
            self.redraw_all_annotations()
            
            # 显示结果
            self.log_message(f"已删除当前帧中ID为 {color_id} 的标注，共 {deleted_count} 项")